Skip to content

Commit c865d7a

Browse files
committed
removed readme.md file changes
1 parent e430a91 commit c865d7a

File tree

10 files changed

+175
-6
lines changed

10 files changed

+175
-6
lines changed

cmd/main.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,33 @@ import (
66
"log/slog"
77
"net/http"
88
"os"
9-
9+
1010
"os/signal"
1111
"syscall"
1212
"time"
1313

1414
"github.com/joshsoftware/code-curiosity-2025/internal/app"
1515
"github.com/joshsoftware/code-curiosity-2025/internal/config"
16+
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/jobs"
1617
)
1718

1819
func main() {
1920
ctx := context.Background()
2021

21-
cfg,err := config.LoadAppConfig()
22+
cfg, err := config.LoadAppConfig()
2223
if err != nil {
2324
slog.Error("error loading app config", "error", err)
2425
return
2526
}
2627

27-
2828
db, err := config.InitDataStore(cfg)
2929
if err != nil {
3030
slog.Error("error initializing database", "error", err)
3131
return
3232
}
3333
defer db.Close()
3434

35-
dependencies := app.InitDependencies(db,cfg)
35+
dependencies := app.InitDependencies(db, cfg)
3636

3737
router := app.NewRouter(dependencies)
3838

@@ -41,6 +41,9 @@ func main() {
4141
Handler: router,
4242
}
4343

44+
// backround job start
45+
jobs.PermanentDeleteJob(db)
46+
4447
serverRunning := make(chan os.Signal, 1)
4548

4649
signal.Notify(

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require (
1818
github.com/hashicorp/go-multierror v1.1.1 // indirect
1919
github.com/joho/godotenv v1.5.1 // indirect
2020
github.com/kr/pretty v0.3.1 // indirect
21+
github.com/robfig/cron/v3 v3.0.0 // indirect
2122
go.uber.org/atomic v1.11.0 // indirect
2223
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
2324
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
3636
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
3737
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
3838
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
39+
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
40+
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
3941
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
4042
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
4143
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=

internal/app/auth/service.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package auth
33
import (
44
"context"
55
"encoding/json"
6+
"fmt"
67
"log/slog"
78

89
"github.com/joshsoftware/code-curiosity-2025/internal/app/user"
@@ -83,6 +84,16 @@ func (s *service) GithubOAuthLoginCallback(ctx context.Context, code string) (st
8384
return "", apperrors.ErrInternalServer
8485
}
8586

87+
// soft delete checker
88+
err = s.userService.RecoverAccountInGracePeriod(ctx, userData.Id)
89+
if err != nil {
90+
slog.Error("error in recovering account in grace period during login", "error", err)
91+
return "", apperrors.ErrInternalServer
92+
}
93+
// token print
94+
95+
fmt.Println(jwtToken)
96+
8697
return jwtToken, nil
8798
}
8899

internal/app/router.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,7 @@ func NewRouter(deps Dependencies) http.Handler {
2020

2121
router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg))
2222

23+
router.HandleFunc("DELETE /api/user/delete", middleware.Authentication(deps.UserHandler.DeleteUser, deps.AppCfg))
24+
2325
return middleware.CorsMiddleware(router, deps.AppCfg)
2426
}

internal/app/user/domain.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ type User struct {
1818
Password string `json:"password"`
1919
IsDeleted bool `json:"is_deleted"`
2020
DeletedAt sql.NullTime `json:"deleted_at"`
21-
CreatedAt time.Time `json:"created_at"`
22-
UpdatedAt time.Time `json:"updated_at"`
21+
CreatedAt time.Time `json:"created_at"`
22+
UpdatedAt time.Time `json:"updated_at"`
2323
}
2424

2525
type CreateUserRequestBody struct {

internal/app/user/handler.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77

88
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
9+
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware"
910
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/response"
1011
)
1112

@@ -15,6 +16,7 @@ type handler struct {
1516

1617
type Handler interface {
1718
UpdateUserEmail(w http.ResponseWriter, r *http.Request)
19+
DeleteUser(w http.ResponseWriter, r *http.Request)
1820
}
1921

2022
func NewHandler(userService Service) Handler {
@@ -44,3 +46,21 @@ func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) {
4446

4547
response.WriteJson(w, http.StatusOK, "email updated successfully", nil)
4648
}
49+
50+
func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
51+
ctx := r.Context()
52+
val := ctx.Value(middleware.UserIdKey)
53+
54+
userID := val.(int)
55+
56+
user, err := h.userService.SoftDeleteUser(ctx, userID)
57+
if err != nil {
58+
slog.Error("failed to softdelete user", "error", err)
59+
status, errorMessage := apperrors.MapError(err)
60+
response.WriteJson(w, status, errorMessage, nil)
61+
return
62+
}
63+
64+
response.WriteJson(w, http.StatusOK, "user scheduled for deletion", user)
65+
66+
}

internal/app/user/service.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package user
33
import (
44
"context"
55
"log/slog"
6+
"time"
67

78
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
89
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware"
@@ -18,6 +19,8 @@ type Service interface {
1819
GetUserByGithubId(ctx context.Context, githubId int) (User, error)
1920
CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error)
2021
UpdateUserEmail(ctx context.Context, email string) error
22+
SoftDeleteUser(ctx context.Context, userID int) (User, error)
23+
RecoverAccountInGracePeriod(ctx context.Context, userID int) error
2124
}
2225

2326
func NewService(userRepository repository.UserRepository) Service {
@@ -74,3 +77,22 @@ func (s *service) UpdateUserEmail(ctx context.Context, email string) error {
7477

7578
return nil
7679
}
80+
81+
func (s *service) SoftDeleteUser(ctx context.Context, userID int) (User, error) {
82+
now := time.Now()
83+
user, err := s.userRepository.MarkUserAsDeleted(ctx, nil, userID, now)
84+
if err != nil {
85+
slog.Error("unable to softdelete user", "error", err)
86+
return User{}, apperrors.ErrInternalServer
87+
}
88+
return User(user), nil
89+
}
90+
91+
func (s *service) RecoverAccountInGracePeriod(ctx context.Context, userID int) error {
92+
err := s.userRepository.AccountScheduledForDelete(ctx, nil, userID)
93+
if err != nil {
94+
slog.Error("failed to recover account in grace period", "error", err)
95+
return err
96+
}
97+
return nil
98+
}

internal/pkg/jobs/cleanUp.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package jobs
2+
3+
import (
4+
"log/slog"
5+
6+
"github.com/jmoiron/sqlx"
7+
"github.com/joshsoftware/code-curiosity-2025/internal/repository"
8+
"github.com/robfig/cron/v3"
9+
)
10+
11+
func PermanentDeleteJob(db *sqlx.DB) {
12+
slog.Info("entering into the cleanup job")
13+
c := cron.New()
14+
_, err := c.AddFunc("36 00 * * *", func() {
15+
slog.Info("Job scheduled for user cleanup from database")
16+
ur := repository.NewUserRepository(db) // pass in *sql.DB or whatever is needed
17+
err := ur.DeleteUser(nil)
18+
if err != nil {
19+
slog.Error("Cleanup job error", "error", err)
20+
} else {
21+
slog.Info("User cleanup Job completed.")
22+
}
23+
})
24+
25+
if err != nil {
26+
slog.Error("failed to start user delete job ", "error", err)
27+
}
28+
29+
c.Start()
30+
}

internal/repository/user.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ type UserRepository interface {
2121
GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error)
2222
CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error)
2323
UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error
24+
MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) (User, error)
25+
AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error
26+
DeleteUser(tx *sqlx.Tx) error
2427
}
2528

2629
func NewUserRepository(db *sqlx.DB) UserRepository {
@@ -120,6 +123,8 @@ func (ur *userRepository) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo
120123
userInfo.GithubUsername,
121124
userInfo.Email,
122125
userInfo.AvatarUrl,
126+
time.Now(),
127+
time.Now(),
123128
).Scan(
124129
&user.Id,
125130
&user.GithubId,
@@ -156,3 +161,76 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user
156161

157162
return nil
158163
}
164+
165+
func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) (User, error) {
166+
executer := ur.BaseRepository.initiateQueryExecuter(tx)
167+
_, err := executer.ExecContext(ctx, `UPDATE users SET is_deleted = TRUE, deleted_at=$1 WHERE id = $2`, deletedAt, userID)
168+
if err != nil {
169+
slog.Error("unable to mark user as deleted", "error", err)
170+
return User{}, apperrors.ErrInternalServer
171+
}
172+
var user User
173+
err = executer.QueryRowContext(ctx, getUserByIdQuery, userID).Scan(
174+
&user.Id,
175+
&user.GithubId,
176+
&user.GithubUsername,
177+
&user.AvatarUrl,
178+
&user.Email,
179+
&user.CurrentActiveGoalId,
180+
&user.CurrentBalance,
181+
&user.IsBlocked,
182+
&user.IsAdmin,
183+
&user.Password,
184+
&user.IsDeleted,
185+
&user.DeletedAt,
186+
&user.CreatedAt,
187+
&user.UpdatedAt,
188+
)
189+
if err != nil {
190+
if errors.Is(err, sql.ErrNoRows) {
191+
slog.Error("user not found", "error", err)
192+
return User{}, apperrors.ErrUserNotFound
193+
}
194+
slog.Error("error occurred while getting user by id", "error", err)
195+
return User{}, apperrors.ErrInternalServer
196+
}
197+
return user, nil
198+
}
199+
200+
func (ur *userRepository) AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error {
201+
var deleteGracePeriod = 90 * 24 * time.Hour
202+
user, err := ur.GetUserById(ctx, tx, userID)
203+
204+
if err != nil {
205+
slog.Error("unable to fetch user by ID ", "error", err)
206+
return apperrors.ErrInternalServer
207+
}
208+
209+
if user.IsDeleted {
210+
var dlt_at time.Time
211+
if !user.DeletedAt.Valid {
212+
return errors.New("invalid deletion state")
213+
} else {
214+
dlt_at = user.DeletedAt.Time
215+
}
216+
217+
if time.Since(dlt_at) >= deleteGracePeriod {
218+
slog.Error("user is permanentaly deleted ", "error", err)
219+
return apperrors.ErrInternalServer
220+
} else {
221+
executer := ur.BaseRepository.initiateQueryExecuter(tx)
222+
_, err := executer.ExecContext(ctx, `UPDATE users SET is_deleted = false, deleted_at = NULL WHERE id = $1`, userID)
223+
slog.Error("unable to reverse the soft delete ", "error", err)
224+
return apperrors.ErrInternalServer
225+
}
226+
}
227+
return nil
228+
}
229+
230+
func (ur *userRepository) DeleteUser(tx *sqlx.Tx) error {
231+
threshold := time.Now().Add(-90 * 1 * time.Second)
232+
executer := ur.BaseRepository.initiateQueryExecuter(tx)
233+
ctx := context.Background()
234+
_, err := executer.ExecContext(ctx, `DELETE FROM users WHERE is_deleted = TRUE AND deleted_at <= $1 `, threshold)
235+
return err
236+
}

0 commit comments

Comments
 (0)