From c865d7add70a49e5c1bf7424f0e6d190e9b55f77 Mon Sep 17 00:00:00 2001 From: Ashok Choudhary Date: Thu, 12 Jun 2025 18:12:55 +0530 Subject: [PATCH 01/11] removed readme.md file changes --- cmd/main.go | 11 +++-- go.mod | 1 + go.sum | 2 + internal/app/auth/service.go | 11 +++++ internal/app/router.go | 2 + internal/app/user/domain.go | 4 +- internal/app/user/handler.go | 20 +++++++++ internal/app/user/service.go | 22 ++++++++++ internal/pkg/jobs/cleanUp.go | 30 ++++++++++++++ internal/repository/user.go | 78 ++++++++++++++++++++++++++++++++++++ 10 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 internal/pkg/jobs/cleanUp.go diff --git a/cmd/main.go b/cmd/main.go index 35ae97a..9bfc734 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,25 +6,25 @@ import ( "log/slog" "net/http" "os" - + "os/signal" "syscall" "time" "github.com/joshsoftware/code-curiosity-2025/internal/app" "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/jobs" ) func main() { ctx := context.Background() - cfg,err := config.LoadAppConfig() + cfg, err := config.LoadAppConfig() if err != nil { slog.Error("error loading app config", "error", err) return } - db, err := config.InitDataStore(cfg) if err != nil { slog.Error("error initializing database", "error", err) @@ -32,7 +32,7 @@ func main() { } defer db.Close() - dependencies := app.InitDependencies(db,cfg) + dependencies := app.InitDependencies(db, cfg) router := app.NewRouter(dependencies) @@ -41,6 +41,9 @@ func main() { Handler: router, } + // backround job start + jobs.PermanentDeleteJob(db) + serverRunning := make(chan os.Signal, 1) signal.Notify( diff --git a/go.mod b/go.mod index e0eeada..c7a9903 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/robfig/cron/v3 v3.0.0 // indirect go.uber.org/atomic v1.11.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index ddd7ccb..a4fa16a 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= +github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= diff --git a/internal/app/auth/service.go b/internal/app/auth/service.go index 3bca6c0..2aeeb91 100644 --- a/internal/app/auth/service.go +++ b/internal/app/auth/service.go @@ -3,6 +3,7 @@ package auth import ( "context" "encoding/json" + "fmt" "log/slog" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" @@ -83,6 +84,16 @@ func (s *service) GithubOAuthLoginCallback(ctx context.Context, code string) (st return "", apperrors.ErrInternalServer } + // soft delete checker + err = s.userService.RecoverAccountInGracePeriod(ctx, userData.Id) + if err != nil { + slog.Error("error in recovering account in grace period during login", "error", err) + return "", apperrors.ErrInternalServer + } + // token print + + fmt.Println(jwtToken) + return jwtToken, nil } diff --git a/internal/app/router.go b/internal/app/router.go index 072a53a..a104288 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,5 +20,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) + router.HandleFunc("DELETE /api/user/delete", middleware.Authentication(deps.UserHandler.DeleteUser, deps.AppCfg)) + return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/app/user/domain.go b/internal/app/user/domain.go index e2d9e6c..d27f5a0 100644 --- a/internal/app/user/domain.go +++ b/internal/app/user/domain.go @@ -18,8 +18,8 @@ type User struct { Password string `json:"password"` IsDeleted bool `json:"is_deleted"` DeletedAt sql.NullTime `json:"deleted_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type CreateUserRequestBody struct { diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go index 00bcd51..191b4fe 100644 --- a/internal/app/user/handler.go +++ b/internal/app/user/handler.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) @@ -15,6 +16,7 @@ type handler struct { type Handler interface { UpdateUserEmail(w http.ResponseWriter, r *http.Request) + DeleteUser(w http.ResponseWriter, r *http.Request) } func NewHandler(userService Service) Handler { @@ -44,3 +46,21 @@ func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "email updated successfully", nil) } + +func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + val := ctx.Value(middleware.UserIdKey) + + userID := val.(int) + + user, err := h.userService.SoftDeleteUser(ctx, userID) + if err != nil { + slog.Error("failed to softdelete user", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "user scheduled for deletion", user) + +} diff --git a/internal/app/user/service.go b/internal/app/user/service.go index 93b8572..6a0fbd2 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -3,6 +3,7 @@ package user import ( "context" "log/slog" + "time" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" @@ -18,6 +19,8 @@ type Service interface { GetUserByGithubId(ctx context.Context, githubId int) (User, error) CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, email string) error + SoftDeleteUser(ctx context.Context, userID int) (User, error) + RecoverAccountInGracePeriod(ctx context.Context, userID int) error } func NewService(userRepository repository.UserRepository) Service { @@ -74,3 +77,22 @@ func (s *service) UpdateUserEmail(ctx context.Context, email string) error { return nil } + +func (s *service) SoftDeleteUser(ctx context.Context, userID int) (User, error) { + now := time.Now() + user, err := s.userRepository.MarkUserAsDeleted(ctx, nil, userID, now) + if err != nil { + slog.Error("unable to softdelete user", "error", err) + return User{}, apperrors.ErrInternalServer + } + return User(user), nil +} + +func (s *service) RecoverAccountInGracePeriod(ctx context.Context, userID int) error { + err := s.userRepository.AccountScheduledForDelete(ctx, nil, userID) + if err != nil { + slog.Error("failed to recover account in grace period", "error", err) + return err + } + return nil +} diff --git a/internal/pkg/jobs/cleanUp.go b/internal/pkg/jobs/cleanUp.go new file mode 100644 index 0000000..50f639e --- /dev/null +++ b/internal/pkg/jobs/cleanUp.go @@ -0,0 +1,30 @@ +package jobs + +import ( + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" + "github.com/robfig/cron/v3" +) + +func PermanentDeleteJob(db *sqlx.DB) { + slog.Info("entering into the cleanup job") + c := cron.New() + _, err := c.AddFunc("36 00 * * *", func() { + slog.Info("Job scheduled for user cleanup from database") + ur := repository.NewUserRepository(db) // pass in *sql.DB or whatever is needed + err := ur.DeleteUser(nil) + if err != nil { + slog.Error("Cleanup job error", "error", err) + } else { + slog.Info("User cleanup Job completed.") + } + }) + + if err != nil { + slog.Error("failed to start user delete job ", "error", err) + } + + c.Start() +} diff --git a/internal/repository/user.go b/internal/repository/user.go index 284ce27..67d7455 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -21,6 +21,9 @@ type UserRepository interface { GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error + MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) (User, error) + AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error + DeleteUser(tx *sqlx.Tx) error } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -120,6 +123,8 @@ func (ur *userRepository) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo userInfo.GithubUsername, userInfo.Email, userInfo.AvatarUrl, + time.Now(), + time.Now(), ).Scan( &user.Id, &user.GithubId, @@ -156,3 +161,76 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user return nil } + +func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) (User, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + _, err := executer.ExecContext(ctx, `UPDATE users SET is_deleted = TRUE, deleted_at=$1 WHERE id = $2`, deletedAt, userID) + if err != nil { + slog.Error("unable to mark user as deleted", "error", err) + return User{}, apperrors.ErrInternalServer + } + var user User + err = executer.QueryRowContext(ctx, getUserByIdQuery, userID).Scan( + &user.Id, + &user.GithubId, + &user.GithubUsername, + &user.AvatarUrl, + &user.Email, + &user.CurrentActiveGoalId, + &user.CurrentBalance, + &user.IsBlocked, + &user.IsAdmin, + &user.Password, + &user.IsDeleted, + &user.DeletedAt, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("user not found", "error", err) + return User{}, apperrors.ErrUserNotFound + } + slog.Error("error occurred while getting user by id", "error", err) + return User{}, apperrors.ErrInternalServer + } + return user, nil +} + +func (ur *userRepository) AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error { + var deleteGracePeriod = 90 * 24 * time.Hour + user, err := ur.GetUserById(ctx, tx, userID) + + if err != nil { + slog.Error("unable to fetch user by ID ", "error", err) + return apperrors.ErrInternalServer + } + + if user.IsDeleted { + var dlt_at time.Time + if !user.DeletedAt.Valid { + return errors.New("invalid deletion state") + } else { + dlt_at = user.DeletedAt.Time + } + + if time.Since(dlt_at) >= deleteGracePeriod { + slog.Error("user is permanentaly deleted ", "error", err) + return apperrors.ErrInternalServer + } else { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + _, err := executer.ExecContext(ctx, `UPDATE users SET is_deleted = false, deleted_at = NULL WHERE id = $1`, userID) + slog.Error("unable to reverse the soft delete ", "error", err) + return apperrors.ErrInternalServer + } + } + return nil +} + +func (ur *userRepository) DeleteUser(tx *sqlx.Tx) error { + threshold := time.Now().Add(-90 * 1 * time.Second) + executer := ur.BaseRepository.initiateQueryExecuter(tx) + ctx := context.Background() + _, err := executer.ExecContext(ctx, `DELETE FROM users WHERE is_deleted = TRUE AND deleted_at <= $1 `, threshold) + return err +} From e54c4fbabaa5aef41aee9e0ca0ec2dac4869a657 Mon Sep 17 00:00:00 2001 From: saurabhkale77 Date: Thu, 26 Jun 2025 00:18:37 +0530 Subject: [PATCH 02/11] WIP - improvements in migrate.go. --- internal/db/migrate.go | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/internal/db/migrate.go b/internal/db/migrate.go index f2020e4..56c1131 100644 --- a/internal/db/migrate.go +++ b/internal/db/migrate.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "os" + "strconv" "time" "github.com/golang-migrate/migrate/v4" @@ -41,8 +42,7 @@ func InitMainDBMigrations(config config.AppConfig) (migration Migration, er erro return } -// MigrationsUp used to make migrations up -func (migration Migration) MigrationsUp() { +func (migration Migration) MigrationsUpAll(){ err := migration.m.Up() if err != nil { if err == migrate.ErrNoChange { @@ -56,6 +56,25 @@ func (migration Migration) MigrationsUp() { slog.Info("Migration up completed") } +func (migration Migration) MigrationsUpWithSteps(steps int){ + +} + +// MigrationsUp used to make migrations up +func (migration Migration) MigrationsUp(steps string) { + if len(steps) == 0 { + migration.MigrationsUpAll() + } else { + stepsCnt, err := strconv.Atoi(steps) + if err != nil { + slog.Error("Failed to parse steps argument to integer", "error", err) + return + } + + migration.MigrationsUpWithSteps(stepsCnt) + } +} + // MigrationsDown used to make migrations down func (migration Migration) MigrationsDown() { err := migration.m.Down() @@ -136,7 +155,7 @@ func main() { } if len(os.Args) < 2 { - slog.Error("Missing action argument. Use 'up' or 'down' or 'create.") + slog.Error("Missing action argument. Use 'up' or 'down' or 'create'.") os.Exit(1) } @@ -149,12 +168,12 @@ func main() { action := os.Args[1] switch action { case "up": - migration.MigrationsUp() + migration.MigrationsUp(os.Args[2]) case "down": migration.MigrationsDown() case "create": migration.CreateMigrationFile(os.Args[2]) default: - slog.Info("Invalid action. Use 'up' or 'down'.") + slog.Info("Invalid action. Use 'up' or 'down' or 'create'.") } } From 9d65177d2af164c4e472a754dc6f7d96cf3ed551 Mon Sep 17 00:00:00 2001 From: saurabhkale77 Date: Mon, 30 Jun 2025 15:36:35 +0530 Subject: [PATCH 03/11] Improvements done in migrate.go for up and down operations of db migrations. --- internal/db/migrate.go | 54 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/internal/db/migrate.go b/internal/db/migrate.go index 56c1131..634e51a 100644 --- a/internal/db/migrate.go +++ b/internal/db/migrate.go @@ -49,15 +49,26 @@ func (migration Migration) MigrationsUpAll(){ slog.Error("No new migrations to apply") return } - slog.Error("*******" + err.Error()) + slog.Error("An error occurred while making migrations up", "error", err) return } - migration.MigrationVersion() + slog.Info("Current migration version:", "version", migration.MigrationVersion()) slog.Info("Migration up completed") } func (migration Migration) MigrationsUpWithSteps(steps int){ + if err := migration.m.Steps(steps); err != nil { + if err == migrate.ErrNoChange { + slog.Error("No new migrations to apply") + return + } + slog.Error("An error occurred while making migrations up", "error", err) + return + } + + slog.Info("Current migration version:", "version", migration.MigrationVersion()) + slog.Info("Migration up completed") } // MigrationsUp used to make migrations up @@ -75,8 +86,7 @@ func (migration Migration) MigrationsUp(steps string) { } } -// MigrationsDown used to make migrations down -func (migration Migration) MigrationsDown() { +func (migration Migration) MigrationsDownAll() { err := migration.m.Down() if err != nil { if err == migrate.ErrNoChange { @@ -84,13 +94,43 @@ func (migration Migration) MigrationsDown() { return } - slog.Error(err.Error()) + slog.Error("An error occurred while making migrations down", "error", err) return } - migration.MigrationVersion() + slog.Info("Current migration version:", "version", migration.MigrationVersion()) slog.Info("Migration down completed") } +func (migration Migration) MigrationsDownWithSteps(steps int) { + if err := migration.m.Steps(-1 * steps); err != nil { + if err == migrate.ErrNoChange { + slog.Error("No migrations to revert") + return + } + + slog.Error("An error occurred while making migrations down", "error", err) + return + } + + slog.Info("Current migration version:", "version", migration.MigrationVersion()) + slog.Info("Migration down completed") +} + +// MigrationsDown used to make migrations down +func (migration Migration) MigrationsDown(steps string) { + if len(steps) == 0 { + migration.MigrationsDownAll() + } else { + stepsCnt, err := strconv.Atoi(steps) + if err != nil { + slog.Error("Failed to parse steps argument to integer", "error", err) + return + } + + migration.MigrationsDownWithSteps(stepsCnt) + } +} + // CreateMigrationFile creates new migration files func (migration Migration) CreateMigrationFile(filename string) (err error) { if len(filename) == 0 { @@ -170,7 +210,7 @@ func main() { case "up": migration.MigrationsUp(os.Args[2]) case "down": - migration.MigrationsDown() + migration.MigrationsDown(os.Args[2]) case "create": migration.CreateMigrationFile(os.Args[2]) default: From 6ee49c93e5ad06f3ff69f6b6c965e1b4a230be47 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 14 Jul 2025 12:44:57 +0530 Subject: [PATCH 04/11] rename GetUserRanks to ListUserRank and get value from context in handler itself --- internal/app/router.go | 2 +- internal/app/user/handler.go | 17 ++++++++++++++--- internal/app/user/service.go | 16 ++++++++-------- internal/pkg/apperrors/errors.go | 3 ++- internal/repository/user.go | 12 ++---------- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/internal/app/router.go b/internal/app/router.go index 02f9c7e..f7047ab 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -27,7 +27,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/languages/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchLanguagePercentInRepo, deps.AppCfg)) - router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.GetAllUsersRank, deps.AppCfg)) + router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.ListUserRanks, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go index 3f085b8..e1000ad 100644 --- a/internal/app/user/handler.go +++ b/internal/app/user/handler.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) @@ -15,7 +16,7 @@ type handler struct { type Handler interface { UpdateUserEmail(w http.ResponseWriter, r *http.Request) - GetAllUsersRank(w http.ResponseWriter, r *http.Request) + ListUserRanks(w http.ResponseWriter, r *http.Request) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) } @@ -47,7 +48,7 @@ func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "email updated successfully", nil) } -func (h *handler) GetAllUsersRank(w http.ResponseWriter, r *http.Request) { +func (h *handler) ListUserRanks(w http.ResponseWriter, r *http.Request) { ctx := r.Context() leaderboard, err := h.userService.GetAllUsersRank(ctx) @@ -64,7 +65,17 @@ func (h *handler) GetAllUsersRank(w http.ResponseWriter, r *http.Request) { func (h *handler) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - currentUserRank, err := h.userService.GetCurrentUserRank(ctx) + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + currentUserRank, err := h.userService.GetCurrentUserRank(ctx, userId) if err != nil { slog.Error("failed to get current user rank", "error", err) status, errorMessage := apperrors.MapError(err) diff --git a/internal/app/user/service.go b/internal/app/user/service.go index 0aed35e..24a0c2d 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -20,7 +20,7 @@ type Service interface { UpdateUserEmail(ctx context.Context, email string) error UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) - GetCurrentUserRank(ctx context.Context) (LeaderboardUser, error) + GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) } func NewService(userRepository repository.UserRepository) Service { @@ -102,22 +102,22 @@ func (s *service) UpdateUserCurrentBalance(ctx context.Context, transaction Tran } func (s *service) GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) { - leaderboard, err := s.userRepository.GetAllUsersRank(ctx, nil) + userRanks, err := s.userRepository.GetAllUsersRank(ctx, nil) if err != nil { slog.Error("error obtaining all users rank", "error", err) return nil, err } - serviceLeaderboard := make([]LeaderboardUser, len(leaderboard)) - for i, l := range leaderboard { - serviceLeaderboard[i] = LeaderboardUser((l)) + Leaderboard := make([]LeaderboardUser, len(userRanks)) + for i, l := range userRanks { + Leaderboard[i] = LeaderboardUser(l) } - return serviceLeaderboard, nil + return Leaderboard, nil } -func (s *service) GetCurrentUserRank(ctx context.Context) (LeaderboardUser, error) { - currentUserRank, err := s.userRepository.GetCurrentUserRank(ctx, nil) +func (s *service) GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) { + currentUserRank, err := s.userRepository.GetCurrentUserRank(ctx, nil, userId) if err != nil { slog.Error("error obtaining current user rank", "error", err) return LeaderboardUser{}, err diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 31c9e49..1ebee57 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -6,6 +6,7 @@ import ( ) var ( + ErrContextValue = errors.New("error obtaining value from context") ErrInternalServer = errors.New("internal server error") ErrInvalidRequestBody = errors.New("invalid or missing parameters in the request body") @@ -52,7 +53,7 @@ var ( func MapError(err error) (statusCode int, errMessage string) { switch err { - case ErrInvalidRequestBody, ErrInvalidQueryParams: + case ErrInvalidRequestBody, ErrInvalidQueryParams, ErrContextValue: return http.StatusBadRequest, err.Error() case ErrUnauthorizedAccess: return http.StatusUnauthorized, err.Error() diff --git a/internal/repository/user.go b/internal/repository/user.go index 82f660a..0a80cbe 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -9,7 +9,6 @@ import ( "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" ) type userRepository struct { @@ -25,7 +24,7 @@ type UserRepository interface { GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) - GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx) (LeaderboardUser, error) + GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -184,14 +183,7 @@ func (ur *userRepository) GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]L return leaderboard, nil } -func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx) (LeaderboardUser, error) { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return LeaderboardUser{}, apperrors.ErrInternalServer - } +func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) { executer := ur.BaseRepository.initiateQueryExecuter(tx) From ecc55c15e30b69ca6ea32d0aecdeb95565346474 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 14 Jul 2025 12:45:36 +0530 Subject: [PATCH 05/11] create index for users_current_balance --- .../1752476063_create-index-users-current-balance.down.sql | 1 + .../1752476063_create-index-users-current-balance.up.sql | 1 + 2 files changed, 2 insertions(+) create mode 100644 internal/db/migrations/1752476063_create-index-users-current-balance.down.sql create mode 100644 internal/db/migrations/1752476063_create-index-users-current-balance.up.sql diff --git a/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql b/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql new file mode 100644 index 0000000..a987013 --- /dev/null +++ b/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql @@ -0,0 +1 @@ +drop index idx_users_current_balance \ No newline at end of file diff --git a/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql b/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql new file mode 100644 index 0000000..a934bf1 --- /dev/null +++ b/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_users_current_balance ON users(current_balance DESC); \ No newline at end of file From 0d0fe6b5534a11864a1882a9fe410240a7d7f385 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 14 Jul 2025 13:09:59 +0530 Subject: [PATCH 06/11] refactor : update namings --- internal/app/contribution/domain.go | 28 ++++++++++++++-------------- internal/app/contribution/handler.go | 6 +++--- internal/app/contribution/service.go | 14 +++++++------- internal/app/router.go | 2 +- internal/repository/contribution.go | 16 ++++++++-------- internal/repository/domain.go | 10 +++++----- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index 9316048..238f5dc 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -51,20 +51,20 @@ type ContributionScore struct { } type Transaction struct { - Id int `db:"id"` - UserId int `db:"user_id"` - ContributionId int `db:"contribution_id"` - IsRedeemed bool `db:"is_redeemed"` - IsGained bool `db:"is_gained"` - TransactedBalance int `db:"transacted_balance"` - TransactedAt time.Time `db:"transacted_at"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + Id int + UserId int + ContributionId int + IsRedeemed bool + IsGained bool + TransactedBalance int + TransactedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time } -type ContributionTypeSummary struct { - ContributionType string `db:"contribution_type"` - ContributionCount int `db:"contribution_count"` - TotalCoins int `db:"total_coins"` - Month time.Time `db:"month"` +type MonthlyContributionSummary struct { + Type string + Count int + TotalCoins int + Month time.Time } diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 85c5e7d..34c08d3 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -14,7 +14,7 @@ type handler struct { type Handler interface { FetchUserContributions(w http.ResponseWriter, r *http.Request) - GetContributionTypeSummaryForMonth(w http.ResponseWriter, r *http.Request) + ListMonthlyContributionSummary(w http.ResponseWriter, r *http.Request) } func NewHandler(contributionService Service) Handler { @@ -37,12 +37,12 @@ func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) response.WriteJson(w, http.StatusOK, "user contributions fetched successfully", userContributions) } -func (h *handler) GetContributionTypeSummaryForMonth(w http.ResponseWriter, r *http.Request) { +func (h *handler) ListMonthlyContributionSummary(w http.ResponseWriter, r *http.Request) { ctx := r.Context() month := r.URL.Query().Get("month") - contributionTypeSummaryForMonth, err := h.contributionService.GetContributionTypeSummaryForMonth(ctx, month) + contributionTypeSummaryForMonth, err := h.contributionService.GetMonthlyContributionSummary(ctx, month) if err != nil { slog.Error("error fetching contribution type summary for month") status, errorMessage := apperrors.MapError(err) diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 88d4d77..082ccbe 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -68,7 +68,7 @@ type Service interface { GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) - GetContributionTypeSummaryForMonth(ctx context.Context, monthParam string) ([]ContributionTypeSummary, error) + GetMonthlyContributionSummary(ctx context.Context, monthParam string) ([]MonthlyContributionSummary, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { @@ -291,7 +291,7 @@ func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEven return Contribution(contribution), nil } -func (s *service) GetContributionTypeSummaryForMonth(ctx context.Context, monthParam string) ([]ContributionTypeSummary, error) { +func (s *service) GetMonthlyContributionSummary(ctx context.Context, monthParam string) ([]MonthlyContributionSummary, error) { month, err := time.Parse("2006-01", monthParam) if err != nil { slog.Error("error parsing month query parameter", "error", err) @@ -304,21 +304,21 @@ func (s *service) GetContributionTypeSummaryForMonth(ctx context.Context, monthP return nil, err } - var contributionTypeSummaryForMonth []ContributionTypeSummary + var monthlyContributionSummary []MonthlyContributionSummary for _, contributionType := range contributionTypes { - contributionTypeSummary, err := s.contributionRepository.GetContributionTypeSummaryForMonth(ctx, nil, contributionType.ContributionType, month) + contributionTypeSummary, err := s.contributionRepository.GetMonthlyContributionTypeSummary(ctx, nil, contributionType.ContributionType, month) if err != nil { if errors.Is(err, apperrors.ErrNoContributionForContributionType) { - contributionTypeSummaryForMonth = append(contributionTypeSummaryForMonth, ContributionTypeSummary{ContributionType: contributionType.ContributionType}) + monthlyContributionSummary = append(monthlyContributionSummary, MonthlyContributionSummary{Type: contributionType.ContributionType}) continue } slog.Error("error fetching contribution type summary", "error", err) return nil, err } - contributionTypeSummaryForMonth = append(contributionTypeSummaryForMonth, ContributionTypeSummary(contributionTypeSummary)) + monthlyContributionSummary = append(monthlyContributionSummary, MonthlyContributionSummary(contributionTypeSummary)) } - return contributionTypeSummaryForMonth, nil + return monthlyContributionSummary, nil } diff --git a/internal/app/router.go b/internal/app/router.go index c61e717..6ace31f 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -21,7 +21,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/monthlyoverview", middleware.Authentication(deps.ContributionHandler.GetContributionTypeSummaryForMonth, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/monthlyoverview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 697028b..79c9f54 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -23,7 +23,7 @@ type ContributionRepository interface { FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) - GetContributionTypeSummaryForMonth(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (ContributionTypeSummary, error) + GetMonthlyContributionTypeSummary(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (MonthlyContributionSummary, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -54,7 +54,7 @@ const ( getAllContributionTypesQuery = `SELECT * from contribution_score` - GetContributionTypeSummaryForMonthQuery = ` + GetMonthlyContributionTypeSummaryQuery = ` SELECT DATE_TRUNC('month', contributed_at) AS month, contribution_type, @@ -160,25 +160,25 @@ func (cr *contributionRepository) GetAllContributionTypes(ctx context.Context, t return contributionTypes, nil } -func (cr *contributionRepository) GetContributionTypeSummaryForMonth(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (ContributionTypeSummary, error) { +func (cr *contributionRepository) GetMonthlyContributionTypeSummary(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (MonthlyContributionSummary, error) { userIdValue := ctx.Value(middleware.UserIdKey) userId, ok := userIdValue.(int) if !ok { slog.Error("error obtaining user id from context") - return ContributionTypeSummary{}, apperrors.ErrInternalServer + return MonthlyContributionSummary{}, apperrors.ErrInternalServer } executer := cr.BaseRepository.initiateQueryExecuter(tx) - var contributionTypeSummary ContributionTypeSummary - err := executer.GetContext(ctx, &contributionTypeSummary, GetContributionTypeSummaryForMonthQuery, userId, contributionType, month) + var contributionTypeSummary MonthlyContributionSummary + err := executer.GetContext(ctx, &contributionTypeSummary, GetMonthlyContributionTypeSummaryQuery, userId, contributionType, month) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return ContributionTypeSummary{}, apperrors.ErrNoContributionForContributionType + return MonthlyContributionSummary{}, apperrors.ErrNoContributionForContributionType } slog.Error("error fetching contribution summary for contribution type", "error", err) - return ContributionTypeSummary{}, apperrors.ErrInternalServer + return MonthlyContributionSummary{}, apperrors.ErrInternalServer } return contributionTypeSummary, nil diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 9d75663..4ac6312 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -86,9 +86,9 @@ type LeaderboardUser struct { Rank int `db:"rank"` } -type ContributionTypeSummary struct { - ContributionType string `db:"contribution_type"` - ContributionCount int `db:"contribution_count"` - TotalCoins int `db:"total_coins"` - Month time.Time `db:"month"` +type MonthlyContributionSummary struct { + Type string `db:"contribution_type"` + Count int `db:"contribution_count"` + TotalCoins int `db:"total_coins"` + Month time.Time `db:"month"` } From 63706a87086e8852e9df44d49eec4723faeb02e3 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 14 Jul 2025 16:53:06 +0530 Subject: [PATCH 07/11] refactor ListMonthlyContributionSummary function --- internal/app/contribution/handler.go | 37 ++++++++++++++++++++++++---- internal/app/contribution/service.go | 33 ++++++------------------- internal/app/router.go | 2 +- internal/pkg/utils/helper.go | 34 +++++++++++++++++++++++++ internal/repository/contribution.go | 32 +++++++----------------- 5 files changed, 84 insertions(+), 54 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 34c08d3..8d604ad 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -5,7 +5,9 @@ import ( "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/utils" ) type handler struct { @@ -28,7 +30,7 @@ func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) userContributions, err := h.contributionService.FetchUserContributions(ctx) if err != nil { - slog.Error("error fetching user contributions") + slog.Error("error fetching user contributions", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -40,15 +42,40 @@ func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) func (h *handler) ListMonthlyContributionSummary(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - month := r.URL.Query().Get("month") + userIdValue := ctx.Value(middleware.UserIdKey) + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + yearVal := r.URL.Query().Get("year") + year, err := utils.ValidateYearQueryParam(yearVal) + if err != nil { + slog.Error("error converting year value to integer", "error", err) + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + monthVal := r.URL.Query().Get("month") + month, err := utils.ValidateMonthQueryParam(monthVal) + if err != nil { + slog.Error("error converting month value to integer", "error", err) + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } - contributionTypeSummaryForMonth, err := h.contributionService.GetMonthlyContributionSummary(ctx, month) + monthlyContributionSummary, err := h.contributionService.ListMonthlyContributionSummary(ctx, year, month, userId) if err != nil { - slog.Error("error fetching contribution type summary for month") + slog.Error("error fetching contribution type summary for month", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return } - response.WriteJson(w, http.StatusOK, "contribution type overview for month fetched successfully", contributionTypeSummaryForMonth) + response.WriteJson(w, http.StatusOK, "contribution type overview for month fetched successfully", monthlyContributionSummary) } diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 082ccbe..610918a 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -3,10 +3,8 @@ package contribution import ( "context" "encoding/json" - "errors" "log/slog" "net/http" - "time" "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" @@ -68,7 +66,7 @@ type Service interface { GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) - GetMonthlyContributionSummary(ctx context.Context, monthParam string) ([]MonthlyContributionSummary, error) + ListMonthlyContributionSummary(ctx context.Context, year int, monthParam int, userId int) ([]MonthlyContributionSummary, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { @@ -291,34 +289,19 @@ func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEven return Contribution(contribution), nil } -func (s *service) GetMonthlyContributionSummary(ctx context.Context, monthParam string) ([]MonthlyContributionSummary, error) { - month, err := time.Parse("2006-01", monthParam) - if err != nil { - slog.Error("error parsing month query parameter", "error", err) - return nil, err - } +func (s *service) ListMonthlyContributionSummary(ctx context.Context, year int, month int, userId int) ([]MonthlyContributionSummary, error) { - contributionTypes, err := s.contributionRepository.GetAllContributionTypes(ctx, nil) + MonthlyContributionSummaries, err := s.contributionRepository.ListMonthlyContributionSummary(ctx, nil, year, month, userId) if err != nil { - slog.Error("error fetching contribution types", "error", err) + slog.Error("error fetching monthly contribution summary", "error", err) return nil, err } - var monthlyContributionSummary []MonthlyContributionSummary - - for _, contributionType := range contributionTypes { - contributionTypeSummary, err := s.contributionRepository.GetMonthlyContributionTypeSummary(ctx, nil, contributionType.ContributionType, month) - if err != nil { - if errors.Is(err, apperrors.ErrNoContributionForContributionType) { - monthlyContributionSummary = append(monthlyContributionSummary, MonthlyContributionSummary{Type: contributionType.ContributionType}) - continue - } - slog.Error("error fetching contribution type summary", "error", err) - return nil, err - } + serviceMonthlyContributionSummaries := make([]MonthlyContributionSummary, len(MonthlyContributionSummaries)) - monthlyContributionSummary = append(monthlyContributionSummary, MonthlyContributionSummary(contributionTypeSummary)) + for i, c := range MonthlyContributionSummaries { + serviceMonthlyContributionSummaries[i] = MonthlyContributionSummary(c) } - return monthlyContributionSummary, nil + return serviceMonthlyContributionSummaries, nil } diff --git a/internal/app/router.go b/internal/app/router.go index 0791715..f2f9242 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -21,7 +21,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/monthlyoverview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) diff --git a/internal/pkg/utils/helper.go b/internal/pkg/utils/helper.go index 3fa0492..e324c8d 100644 --- a/internal/pkg/utils/helper.go +++ b/internal/pkg/utils/helper.go @@ -5,7 +5,11 @@ import ( "io" "log/slog" "net/http" + "strconv" "strings" + "time" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" ) func FormatIntSliceForQuery(ids []int) string { @@ -43,3 +47,33 @@ func DoGet(httpClient *http.Client, url string, headers map[string]string) ([]by return body, nil } + +func ValidateYearQueryParam(yearVal string) (int, error) { + year, err := strconv.Atoi(yearVal) + if err != nil { + slog.Error("error converting year string value to int") + return 0, err + } + + if year < 2025 || year > time.Now().Year() { + slog.Error("invalid year value") + return 0, apperrors.ErrInvalidQueryParams + } + + return year, nil +} + +func ValidateMonthQueryParam(monthVal string) (int, error) { + month, err := strconv.Atoi(monthVal) + if err != nil { + slog.Error("error converting month string value to int") + return 0, err + } + + if month < 0 || month > 12 { + slog.Error("invalid month value") + return 0, apperrors.ErrInvalidQueryParams + } + + return month, nil +} diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 79c9f54..da5163a 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -5,7 +5,6 @@ import ( "database/sql" "errors" "log/slog" - "time" "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" @@ -23,7 +22,7 @@ type ContributionRepository interface { FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) - GetMonthlyContributionTypeSummary(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (MonthlyContributionSummary, error) + ListMonthlyContributionSummary(ctx context.Context, tx *sqlx.Tx, year int, month int, userId int) ([]MonthlyContributionSummary, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -54,17 +53,15 @@ const ( getAllContributionTypesQuery = `SELECT * from contribution_score` - GetMonthlyContributionTypeSummaryQuery = ` + getMonthlyContributionSummaryQuery = ` SELECT DATE_TRUNC('month', contributed_at) AS month, - contribution_type, + contribution_type, COUNT(*) AS contribution_count, SUM(balance_change) AS total_coins FROM contributions WHERE user_id = $1 - AND contribution_type = $2 - AND contributed_at >= DATE_TRUNC('month', $3::timestamptz) - AND contributed_at < DATE_TRUNC('month', $3::timestamptz) + INTERVAL '1 month' + AND DATE_TRUNC('month', contributed_at) = MAKE_DATE($2, $3, 1)::timestamptz GROUP BY month, contribution_type;` ) @@ -160,25 +157,14 @@ func (cr *contributionRepository) GetAllContributionTypes(ctx context.Context, t return contributionTypes, nil } -func (cr *contributionRepository) GetMonthlyContributionTypeSummary(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (MonthlyContributionSummary, error) { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return MonthlyContributionSummary{}, apperrors.ErrInternalServer - } - +func (cr *contributionRepository) ListMonthlyContributionSummary(ctx context.Context, tx *sqlx.Tx, year int, month int, userId int) ([]MonthlyContributionSummary, error) { executer := cr.BaseRepository.initiateQueryExecuter(tx) - var contributionTypeSummary MonthlyContributionSummary - err := executer.GetContext(ctx, &contributionTypeSummary, GetMonthlyContributionTypeSummaryQuery, userId, contributionType, month) + var contributionTypeSummary []MonthlyContributionSummary + err := executer.SelectContext(ctx, &contributionTypeSummary, getMonthlyContributionSummaryQuery, userId, month) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return MonthlyContributionSummary{}, apperrors.ErrNoContributionForContributionType - } - slog.Error("error fetching contribution summary for contribution type", "error", err) - return MonthlyContributionSummary{}, apperrors.ErrInternalServer + slog.Error("error fetching monthly contribution summary for user", "error", err) + return nil, apperrors.ErrInternalServer } return contributionTypeSummary, nil From 7fc91d090d8d87611fd1d49a4566dd9e00907502 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 14 Jul 2025 18:36:48 +0530 Subject: [PATCH 08/11] refactor soft delete user --- go.mod | 1 - go.sum | 2 -- internal/app/router.go | 2 +- internal/app/user/handler.go | 21 +++++++++++-------- internal/app/user/service.go | 10 ++++----- internal/repository/user.go | 39 +++++++++--------------------------- 6 files changed, 28 insertions(+), 47 deletions(-) diff --git a/go.mod b/go.mod index 5e33a45..e73f2e2 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,6 @@ require ( go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect - github.com/robfig/cron/v3 v3.0.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect diff --git a/go.sum b/go.sum index 4aadde7..a599248 100644 --- a/go.sum +++ b/go.sum @@ -133,8 +133,6 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgm github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= -github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= diff --git a/internal/app/router.go b/internal/app/router.go index cdf5fc4..76759a1 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -19,7 +19,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/auth/user", middleware.Authentication(deps.AuthHandler.GetLoggedInUser, deps.AppCfg)) router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) - router.HandleFunc("DELETE /api/user/delete", middleware.Authentication(deps.UserHandler.DeleteUser, deps.AppCfg)) + router.HandleFunc("DELETE /api/v1/user/delete/{user_id}", middleware.Authentication(deps.UserHandler.SoftDeleteUser, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go index 801102c..1a0dc22 100644 --- a/internal/app/user/handler.go +++ b/internal/app/user/handler.go @@ -7,7 +7,6 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) @@ -17,7 +16,7 @@ type handler struct { type Handler interface { UpdateUserEmail(w http.ResponseWriter, r *http.Request) - DeleteUser(w http.ResponseWriter, r *http.Request) + SoftDeleteUser(w http.ResponseWriter, r *http.Request) ListUserRanks(w http.ResponseWriter, r *http.Request) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) } @@ -50,13 +49,20 @@ func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "email updated successfully", nil) } -func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) { +func (h *handler) SoftDeleteUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - val := ctx.Value(middleware.UserIdKey) - userID := val.(int) + userIdValue := ctx.Value(middleware.UserIdKey) - user, err := h.userService.SoftDeleteUser(ctx, userID) + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + err := h.userService.SoftDeleteUser(ctx, userId) if err != nil { slog.Error("failed to softdelete user", "error", err) status, errorMessage := apperrors.MapError(err) @@ -64,8 +70,7 @@ func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) { return } - response.WriteJson(w, http.StatusOK, "user scheduled for deletion", user) - + response.WriteJson(w, http.StatusOK, "user scheduled for deletion", nil) } func (h *handler) ListUserRanks(w http.ResponseWriter, r *http.Request) { diff --git a/internal/app/user/service.go b/internal/app/user/service.go index ff137a5..0fe83ec 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -19,7 +19,7 @@ type Service interface { GetUserByGithubId(ctx context.Context, githubId int) (User, error) CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, email string) error - SoftDeleteUser(ctx context.Context, userID int) (User, error) + SoftDeleteUser(ctx context.Context, userID int) error RecoverAccountInGracePeriod(ctx context.Context, userID int) error UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) @@ -81,14 +81,14 @@ func (s *service) UpdateUserEmail(ctx context.Context, email string) error { return nil } -func (s *service) SoftDeleteUser(ctx context.Context, userID int) (User, error) { +func (s *service) SoftDeleteUser(ctx context.Context, userID int) error { now := time.Now() - user, err := s.userRepository.MarkUserAsDeleted(ctx, nil, userID, now) + err := s.userRepository.MarkUserAsDeleted(ctx, nil, userID, now) if err != nil { slog.Error("unable to softdelete user", "error", err) - return User{}, apperrors.ErrInternalServer + return apperrors.ErrInternalServer } - return User(user), nil + return nil } func (s *service) RecoverAccountInGracePeriod(ctx context.Context, userID int) error { diff --git a/internal/repository/user.go b/internal/repository/user.go index e5d2129..f1ad788 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -21,7 +21,7 @@ type UserRepository interface { GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error - MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) (User, error) + MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) error AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error DeleteUser(tx *sqlx.Tx) error GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) @@ -53,6 +53,8 @@ const ( updateEmailQuery = "UPDATE users SET email=$1, updated_at=$2 where id=$3" + markUserAsDeletedQuery = "UPDATE users SET is_deleted = TRUE, deleted_at=$1 WHERE id = $2" + getAllUsersGithubIdQuery = "SELECT github_id from users" updateUserCurrentBalanceQuery = "UPDATE users SET current_balance=$1, updated_at=$2 where id=$3" @@ -148,39 +150,16 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user return nil } -func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) (User, error) { +func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) error { executer := ur.BaseRepository.initiateQueryExecuter(tx) - _, err := executer.ExecContext(ctx, `UPDATE users SET is_deleted = TRUE, deleted_at=$1 WHERE id = $2`, deletedAt, userID) + + _, err := executer.ExecContext(ctx, markUserAsDeletedQuery, deletedAt, userID) if err != nil { slog.Error("unable to mark user as deleted", "error", err) - return User{}, apperrors.ErrInternalServer - } - var user User - err = executer.QueryRowContext(ctx, getUserByIdQuery, userID).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - slog.Error("user not found", "error", err) - return User{}, apperrors.ErrUserNotFound - } - slog.Error("error occurred while getting user by id", "error", err) - return User{}, apperrors.ErrInternalServer + return apperrors.ErrInternalServer } - return user, nil + + return nil } func (ur *userRepository) AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error { From 29054b1297c596c26648bb3d76f4c4ef27c67a29 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 15 Jul 2025 15:43:53 +0530 Subject: [PATCH 09/11] refactor user deletion feature --- cmd/main.go | 6 +-- internal/app/auth/service.go | 15 +++--- internal/app/cronJob/cleanupJob.go | 32 +++++++++++++ internal/app/cronJob/init.go | 4 +- internal/app/dependencies.go | 2 + internal/app/user/service.go | 15 +++++- internal/pkg/jobs/cleanUp.go | 30 ------------ internal/repository/user.go | 77 +++++++++++++++++++----------- 8 files changed, 105 insertions(+), 76 deletions(-) create mode 100644 internal/app/cronJob/cleanupJob.go delete mode 100644 internal/pkg/jobs/cleanUp.go diff --git a/cmd/main.go b/cmd/main.go index 24ea96c..c0fc99e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -14,7 +14,6 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app" "github.com/joshsoftware/code-curiosity-2025/internal/app/cronJob" "github.com/joshsoftware/code-curiosity-2025/internal/config" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/jobs" ) func main() { @@ -46,16 +45,13 @@ func main() { router := app.NewRouter(dependencies) newCronSchedular := cronJob.NewCronSchedular() - newCronSchedular.InitCronJobs(dependencies.ContributionService) + newCronSchedular.InitCronJobs(dependencies.ContributionService, dependencies.UserService) server := http.Server{ Addr: fmt.Sprintf(":%s", cfg.HTTPServer.Port), Handler: router, } - // backround job start - jobs.PermanentDeleteJob(db) - serverRunning := make(chan os.Signal, 1) signal.Notify( diff --git a/internal/app/auth/service.go b/internal/app/auth/service.go index 2aeeb91..c0cbeb4 100644 --- a/internal/app/auth/service.go +++ b/internal/app/auth/service.go @@ -3,7 +3,6 @@ package auth import ( "context" "encoding/json" - "fmt" "log/slog" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" @@ -84,15 +83,13 @@ func (s *service) GithubOAuthLoginCallback(ctx context.Context, code string) (st return "", apperrors.ErrInternalServer } - // soft delete checker - err = s.userService.RecoverAccountInGracePeriod(ctx, userData.Id) - if err != nil { - slog.Error("error in recovering account in grace period during login", "error", err) - return "", apperrors.ErrInternalServer + if userData.IsDeleted { + err = s.userService.RecoverAccountInGracePeriod(ctx, userData.Id) + if err != nil { + slog.Error("error in recovering account in grace period during login", "error", err) + return "", apperrors.ErrInternalServer + } } - // token print - - fmt.Println(jwtToken) return jwtToken, nil } diff --git a/internal/app/cronJob/cleanupJob.go b/internal/app/cronJob/cleanupJob.go new file mode 100644 index 0000000..8bb1574 --- /dev/null +++ b/internal/app/cronJob/cleanupJob.go @@ -0,0 +1,32 @@ +package cronJob + +import ( + "context" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" +) + +type CleanupJob struct { + CronJob + userService user.Service +} + +func NewCleanupJob(userService user.Service) *CleanupJob { + return &CleanupJob{ + userService: userService, + CronJob: CronJob{Name: "User Cleanup Job Daily"}, + } +} + +func (c *CleanupJob) Schedule(s *CronSchedular) error { + _, err := s.cron.AddFunc("15 13 * * *", func() { c.Execute(context.Background(), c.run) }) + if err != nil { + return err + } + + return nil +} + +func (c *CleanupJob) run(ctx context.Context) { + c.userService.HardDeleteUsers(ctx) +} diff --git a/internal/app/cronJob/init.go b/internal/app/cronJob/init.go index 7fba0f3..77d9f78 100644 --- a/internal/app/cronJob/init.go +++ b/internal/app/cronJob/init.go @@ -5,6 +5,7 @@ import ( "time" "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" "github.com/robfig/cron/v3" ) @@ -23,9 +24,10 @@ func NewCronSchedular() *CronSchedular { } } -func (c *CronSchedular) InitCronJobs(contributionService contribution.Service) { +func (c *CronSchedular) InitCronJobs(contributionService contribution.Service, userService user.Service) { jobs := []Job{ NewDailyJob(contributionService), + NewCleanupJob(userService), } for _, job := range jobs { diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 86d4c3d..cb95a53 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -18,6 +18,7 @@ import ( type Dependencies struct { ContributionService contribution.Service + UserService user.Service AuthHandler auth.Handler UserHandler user.Handler ContributionHandler contribution.Handler @@ -47,6 +48,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque return Dependencies{ ContributionService: contributionService, + UserService: userService, AuthHandler: authHandler, UserHandler: userHandler, RepositoryHandler: repositoryHandler, diff --git a/internal/app/user/service.go b/internal/app/user/service.go index 0fe83ec..fa37e18 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -19,7 +19,8 @@ type Service interface { GetUserByGithubId(ctx context.Context, githubId int) (User, error) CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, email string) error - SoftDeleteUser(ctx context.Context, userID int) error + SoftDeleteUser(ctx context.Context, userId int) error + HardDeleteUsers(ctx context.Context) error RecoverAccountInGracePeriod(ctx context.Context, userID int) error UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) @@ -91,8 +92,18 @@ func (s *service) SoftDeleteUser(ctx context.Context, userID int) error { return nil } +func (s *service) HardDeleteUsers(ctx context.Context) error { + err := s.userRepository.HardDeleteUsers(ctx, nil) + if err != nil { + slog.Error("error deleting users that are soft deleted for more than three months", "error", err) + return err + } + + return nil +} + func (s *service) RecoverAccountInGracePeriod(ctx context.Context, userID int) error { - err := s.userRepository.AccountScheduledForDelete(ctx, nil, userID) + err := s.userRepository.RecoverAccountInGracePeriod(ctx, nil, userID) if err != nil { slog.Error("failed to recover account in grace period", "error", err) return err diff --git a/internal/pkg/jobs/cleanUp.go b/internal/pkg/jobs/cleanUp.go deleted file mode 100644 index 50f639e..0000000 --- a/internal/pkg/jobs/cleanUp.go +++ /dev/null @@ -1,30 +0,0 @@ -package jobs - -import ( - "log/slog" - - "github.com/jmoiron/sqlx" - "github.com/joshsoftware/code-curiosity-2025/internal/repository" - "github.com/robfig/cron/v3" -) - -func PermanentDeleteJob(db *sqlx.DB) { - slog.Info("entering into the cleanup job") - c := cron.New() - _, err := c.AddFunc("36 00 * * *", func() { - slog.Info("Job scheduled for user cleanup from database") - ur := repository.NewUserRepository(db) // pass in *sql.DB or whatever is needed - err := ur.DeleteUser(nil) - if err != nil { - slog.Error("Cleanup job error", "error", err) - } else { - slog.Info("User cleanup Job completed.") - } - }) - - if err != nil { - slog.Error("failed to start user delete job ", "error", err) - } - - c.Start() -} diff --git a/internal/repository/user.go b/internal/repository/user.go index f1ad788..72968af 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -22,8 +22,8 @@ type UserRepository interface { CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) error - AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error - DeleteUser(tx *sqlx.Tx) error + RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userID int) error + HardDeleteUsers(ctx context.Context, tx *sqlx.Tx) error GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) @@ -53,7 +53,11 @@ const ( updateEmailQuery = "UPDATE users SET email=$1, updated_at=$2 where id=$3" - markUserAsDeletedQuery = "UPDATE users SET is_deleted = TRUE, deleted_at=$1 WHERE id = $2" + markUserAsDeletedQuery = "UPDATE users SET is_deleted = TRUE, deleted_at=$1 where id = $2" + + recoverAccountInGracePeriodQuery = "UPDATE users SET is_deleted = false, deleted_at = NULL where id = $1" + + hardDeleteUsersQuery = "DELETE FROM users WHERE is_deleted = TRUE AND deleted_at <= $1" getAllUsersGithubIdQuery = "SELECT github_id from users" @@ -162,41 +166,29 @@ func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, us return nil } -func (ur *userRepository) AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error { - var deleteGracePeriod = 90 * 24 * time.Hour - user, err := ur.GetUserById(ctx, tx, userID) +func (ur *userRepository) RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userID int) error { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + _, err := executer.ExecContext(ctx, recoverAccountInGracePeriodQuery, userID) if err != nil { - slog.Error("unable to fetch user by ID ", "error", err) + slog.Error("unable to reverse the soft delete ", "error", err) return apperrors.ErrInternalServer } - if user.IsDeleted { - var dlt_at time.Time - if !user.DeletedAt.Valid { - return errors.New("invalid deletion state") - } else { - dlt_at = user.DeletedAt.Time - } - - if time.Since(dlt_at) >= deleteGracePeriod { - slog.Error("user is permanentaly deleted ", "error", err) - return apperrors.ErrInternalServer - } else { - executer := ur.BaseRepository.initiateQueryExecuter(tx) - _, err := executer.ExecContext(ctx, `UPDATE users SET is_deleted = false, deleted_at = NULL WHERE id = $1`, userID) - slog.Error("unable to reverse the soft delete ", "error", err) - return apperrors.ErrInternalServer - } - } return nil } -func (ur *userRepository) DeleteUser(tx *sqlx.Tx) error { - threshold := time.Now().Add(-90 * 1 * time.Second) +func (ur *userRepository) HardDeleteUsers(ctx context.Context, tx *sqlx.Tx) error { executer := ur.BaseRepository.initiateQueryExecuter(tx) - ctx := context.Background() - _, err := executer.ExecContext(ctx, `DELETE FROM users WHERE is_deleted = TRUE AND deleted_at <= $1 `, threshold) + + threshold := time.Now().Add(-90 * 1 * time.Second) + + _, err := executer.ExecContext(ctx, hardDeleteUsersQuery, threshold) + if err != nil { + slog.Error("error deleting users that are soft deleted for more than three months", "error", err) + return apperrors.ErrInternalServer + } + return err } @@ -251,3 +243,30 @@ func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, u return currentUserRank, nil } + +// DELETE FROM leaderboard_hourly WHERE user_id = 1; +// DELETE FROM badges WHERE user_id = 1; +// DELETE FROM goal_contribution WHERE set_by_user_id = 1; +// DELETE FROM contribution_score WHERE admin_id = 1; + +// WITH user_contributions AS ( +// SELECT id FROM contributions WHERE user_id = 1 +// ) + +// DELETE FROM transactions +// WHERE contribution_id IN (SELECT id FROM user_contributions); + +// DELETE FROM transactions WHERE user_id = 1; + +// DELETE FROM summary WHERE user_id = 1; + +// DELETE FROM summary +// WHERE contribution_id IN (SELECT id FROM user_contributions); + +// select id as idstodelete from repositories where id in (select repository_id as repos from contributions where user_id=1) + +// delete from repositories where id in idstodelete and id not in (select repository_id from contributions where user_id=1) + +// DELETE FROM contributions WHERE user_id = 1; + +// DELETE FROM users WHERE id = 1; From 7390be297a04ae5fc2235d2d8785af922fbda020 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 16 Jul 2025 12:42:12 +0530 Subject: [PATCH 10/11] Merge branch 'main' of https://github.com/joshsoftware/code-curiosity-2025 into feat/Delete --- internal/app/cronJob/cleanupJob.go | 2 +- internal/db/migrate.go | 18 +++++++++++------- internal/db/migrations/1748862201_init.up.sql | 14 +++++++------- ...1016438_allow-null-contribution-id.down.sql | 2 +- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/internal/app/cronJob/cleanupJob.go b/internal/app/cronJob/cleanupJob.go index 8bb1574..d87b3c9 100644 --- a/internal/app/cronJob/cleanupJob.go +++ b/internal/app/cronJob/cleanupJob.go @@ -19,7 +19,7 @@ func NewCleanupJob(userService user.Service) *CleanupJob { } func (c *CleanupJob) Schedule(s *CronSchedular) error { - _, err := s.cron.AddFunc("15 13 * * *", func() { c.Execute(context.Background(), c.run) }) + _, err := s.cron.AddFunc("00 18 * * *", func() { c.Execute(context.Background(), c.run) }) if err != nil { return err } diff --git a/internal/db/migrate.go b/internal/db/migrate.go index 634e51a..6b9a159 100644 --- a/internal/db/migrate.go +++ b/internal/db/migrate.go @@ -42,7 +42,7 @@ func InitMainDBMigrations(config config.AppConfig) (migration Migration, er erro return } -func (migration Migration) MigrationsUpAll(){ +func (migration Migration) MigrationsUpAll() { err := migration.m.Up() if err != nil { if err == migrate.ErrNoChange { @@ -56,7 +56,7 @@ func (migration Migration) MigrationsUpAll(){ slog.Info("Migration up completed") } -func (migration Migration) MigrationsUpWithSteps(steps int){ +func (migration Migration) MigrationsUpWithSteps(steps int) { if err := migration.m.Steps(steps); err != nil { if err == migrate.ErrNoChange { slog.Error("No new migrations to apply") @@ -65,7 +65,7 @@ func (migration Migration) MigrationsUpWithSteps(steps int){ slog.Error("An error occurred while making migrations up", "error", err) return - } + } slog.Info("Current migration version:", "version", migration.MigrationVersion()) slog.Info("Migration up completed") @@ -110,7 +110,7 @@ func (migration Migration) MigrationsDownWithSteps(steps int) { slog.Error("An error occurred while making migrations down", "error", err) return - } + } slog.Info("Current migration version:", "version", migration.MigrationVersion()) slog.Info("Migration down completed") @@ -206,13 +206,17 @@ func main() { } action := os.Args[1] + var steps string + if len(os.Args) > 2 { + steps = os.Args[2] + } switch action { case "up": - migration.MigrationsUp(os.Args[2]) + migration.MigrationsUp(steps) case "down": - migration.MigrationsDown(os.Args[2]) + migration.MigrationsDown(steps) case "create": - migration.CreateMigrationFile(os.Args[2]) + migration.CreateMigrationFile(steps) default: slog.Info("Invalid action. Use 'up' or 'down' or 'create'.") } diff --git a/internal/db/migrations/1748862201_init.up.sql b/internal/db/migrations/1748862201_init.up.sql index 476693a..c06fb1d 100644 --- a/internal/db/migrations/1748862201_init.up.sql +++ b/internal/db/migrations/1748862201_init.up.sql @@ -113,28 +113,28 @@ CREATE TABLE "goal_contribution"( ); ALTER TABLE - "goal_contribution" ADD CONSTRAINT "goal_contribution_set_by_user_id_foreign" FOREIGN KEY("set_by_user_id") REFERENCES "users"("id"); + "goal_contribution" ADD CONSTRAINT "goal_contribution_set_by_user_id_foreign" FOREIGN KEY("set_by_user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "goal_contribution" ADD CONSTRAINT "goal_contribution_contribution_score_id_foreign" FOREIGN KEY("contribution_score_id") REFERENCES "contribution_score"("id"); ALTER TABLE - "contribution_score" ADD CONSTRAINT "contribution_score_admin_id_foreign" FOREIGN KEY("admin_id") REFERENCES "users"("id"); + "contribution_score" ADD CONSTRAINT "contribution_score_admin_id_foreign" FOREIGN KEY("admin_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE - "summary" ADD CONSTRAINT "summary_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "summary" ADD CONSTRAINT "summary_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE - "transactions" ADD CONSTRAINT "transactions_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "transactions" ADD CONSTRAINT "transactions_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "contributions" ADD CONSTRAINT "contributions_contribution_score_id_foreign" FOREIGN KEY("contribution_score_id") REFERENCES "contribution_score"("id"); ALTER TABLE - "badges" ADD CONSTRAINT "badges_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "badges" ADD CONSTRAINT "badges_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "goal_contribution" ADD CONSTRAINT "goal_contribution_goal_id_foreign" FOREIGN KEY("goal_id") REFERENCES "goal"("id"); ALTER TABLE "transactions" ADD CONSTRAINT "transactions_contribution_id_foreign" FOREIGN KEY("contribution_id") REFERENCES "contributions"("id"); ALTER TABLE - "contributions" ADD CONSTRAINT "contributions_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "contributions" ADD CONSTRAINT "contributions_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "contributions" ADD CONSTRAINT "contributions_repository_id_foreign" FOREIGN KEY("repository_id") REFERENCES "repositories"("id"); ALTER TABLE - "leaderboard_hourly" ADD CONSTRAINT "leaderboard_hourly_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "leaderboard_hourly" ADD CONSTRAINT "leaderboard_hourly_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "summary" ADD CONSTRAINT "summary_contribution_id_foreign" FOREIGN KEY("contribution_id") REFERENCES "contributions"("id"); \ No newline at end of file diff --git a/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql b/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql index c44d425..ff2f133 100644 --- a/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql +++ b/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql @@ -1,2 +1,2 @@ ALTER TABLE transactions -ALTER COLUMN contribution_id SET NOT NULL DEFAULT 0; \ No newline at end of file +ALTER COLUMN contribution_id SET NOT NULL; \ No newline at end of file From 8612d285c6031230b37523ca443775cdb2867f19 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 16 Jul 2025 16:32:14 +0530 Subject: [PATCH 11/11] remove unnecesssary comments --- internal/repository/user.go | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/internal/repository/user.go b/internal/repository/user.go index 72968af..d9a6cc9 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -242,31 +242,4 @@ func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, u } return currentUserRank, nil -} - -// DELETE FROM leaderboard_hourly WHERE user_id = 1; -// DELETE FROM badges WHERE user_id = 1; -// DELETE FROM goal_contribution WHERE set_by_user_id = 1; -// DELETE FROM contribution_score WHERE admin_id = 1; - -// WITH user_contributions AS ( -// SELECT id FROM contributions WHERE user_id = 1 -// ) - -// DELETE FROM transactions -// WHERE contribution_id IN (SELECT id FROM user_contributions); - -// DELETE FROM transactions WHERE user_id = 1; - -// DELETE FROM summary WHERE user_id = 1; - -// DELETE FROM summary -// WHERE contribution_id IN (SELECT id FROM user_contributions); - -// select id as idstodelete from repositories where id in (select repository_id as repos from contributions where user_id=1) - -// delete from repositories where id in idstodelete and id not in (select repository_id from contributions where user_id=1) - -// DELETE FROM contributions WHERE user_id = 1; - -// DELETE FROM users WHERE id = 1; +} \ No newline at end of file