From c865d7add70a49e5c1bf7424f0e6d190e9b55f77 Mon Sep 17 00:00:00 2001 From: Ashok Choudhary Date: Thu, 12 Jun 2025 18:12:55 +0530 Subject: [PATCH 1/2] 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 80bc6821dee617481233917e9867d0f73ec6162f Mon Sep 17 00:00:00 2001 From: Ashok Choudhary Date: Mon, 16 Jun 2025 13:18:57 +0530 Subject: [PATCH 2/2] implement user summary --- internal/app/router.go | 2 ++ internal/app/user/handler.go | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/internal/app/router.go b/internal/app/router.go index a104288..9181eec 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -22,5 +22,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("DELETE /api/user/delete", middleware.Authentication(deps.UserHandler.DeleteUser, deps.AppCfg)) + router.HandleFunc("GET /api/user/summary", middleware.Authentication(deps.UserHandler.UserSummary, deps.AppCfg)) + return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go index 191b4fe..ca6f06d 100644 --- a/internal/app/user/handler.go +++ b/internal/app/user/handler.go @@ -64,3 +64,8 @@ func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "user scheduled for deletion", user) } + +func (h *handler) UserSummary(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + +}