Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ 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),
Expand Down
8 changes: 8 additions & 0 deletions internal/app/auth/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ func (s *service) GithubOAuthLoginCallback(ctx context.Context, code string) (st
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
}
}

return jwtToken, nil
}

Expand Down
28 changes: 14 additions & 14 deletions internal/app/contribution/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
41 changes: 34 additions & 7 deletions internal/app/contribution/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -14,7 +16,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 {
Expand All @@ -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
Expand All @@ -37,18 +39,43 @@ 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")
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.GetContributionTypeSummaryForMonth(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)
}
33 changes: 8 additions & 25 deletions internal/app/contribution/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
GetContributionTypeSummaryForMonth(ctx context.Context, monthParam string) ([]ContributionTypeSummary, 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 {
Expand Down Expand Up @@ -291,34 +289,19 @@ func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEven
return Contribution(contribution), nil
}

func (s *service) GetContributionTypeSummaryForMonth(ctx context.Context, monthParam string) ([]ContributionTypeSummary, 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 contributionTypeSummaryForMonth []ContributionTypeSummary

for _, contributionType := range contributionTypes {
contributionTypeSummary, err := s.contributionRepository.GetContributionTypeSummaryForMonth(ctx, nil, contributionType.ContributionType, month)
if err != nil {
if errors.Is(err, apperrors.ErrNoContributionForContributionType) {
contributionTypeSummaryForMonth = append(contributionTypeSummaryForMonth, ContributionTypeSummary{ContributionType: contributionType.ContributionType})
continue
}
slog.Error("error fetching contribution type summary", "error", err)
return nil, err
}
serviceMonthlyContributionSummaries := make([]MonthlyContributionSummary, len(MonthlyContributionSummaries))

contributionTypeSummaryForMonth = append(contributionTypeSummaryForMonth, ContributionTypeSummary(contributionTypeSummary))
for i, c := range MonthlyContributionSummaries {
serviceMonthlyContributionSummaries[i] = MonthlyContributionSummary(c)
}

return contributionTypeSummaryForMonth, nil
return serviceMonthlyContributionSummaries, nil
}
32 changes: 32 additions & 0 deletions internal/app/cronJob/cleanupJob.go
Original file line number Diff line number Diff line change
@@ -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("00 18 * * *", 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)
}
4 changes: 3 additions & 1 deletion internal/app/cronJob/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions internal/app/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

type Dependencies struct {
ContributionService contribution.Service
UserService user.Service
AuthHandler auth.Handler
UserHandler user.Handler
ContributionHandler contribution.Handler
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions internal/app/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ 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/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/monthlyoverview", middleware.Authentication(deps.ContributionHandler.GetContributionTypeSummaryForMonth, 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))
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)
Expand Down
42 changes: 39 additions & 3 deletions internal/app/user/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -15,7 +16,8 @@ type handler struct {

type Handler interface {
UpdateUserEmail(w http.ResponseWriter, r *http.Request)
GetAllUsersRank(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)
}

Expand Down Expand Up @@ -47,7 +49,31 @@ 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) SoftDeleteUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

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
}

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", nil)
}

func (h *handler) ListUserRanks(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

leaderboard, err := h.userService.GetAllUsersRank(ctx)
Expand All @@ -64,7 +90,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)
Expand Down
Loading