-
Notifications
You must be signed in to change notification settings - Fork 0
Deletion of user #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/monthly-overview
Are you sure you want to change the base?
Deletion of user #14
Changes from 1 commit
c865d7a
e54c4fb
9d65177
6ee49c9
ecc55c1
0d0fe6b
cf845de
63706a8
466144c
7fc91d0
29054b1
14e60a0
402fba4
7390be2
8612d28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| } | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only call recover if userData.IsDeleted is true