package thing import ( "strconv" "time" auth "donetick.com/core/internal/authorization" chRepo "donetick.com/core/internal/chore/repo" cRepo "donetick.com/core/internal/circle/repo" nRepo "donetick.com/core/internal/notifier/repo" nps "donetick.com/core/internal/notifier/service" tModel "donetick.com/core/internal/thing/model" tRepo "donetick.com/core/internal/thing/repo" "donetick.com/core/logging" jwt "github.com/appleboy/gin-jwt/v2" "github.com/gin-gonic/gin" ) type Handler struct { choreRepo *chRepo.ChoreRepository circleRepo *cRepo.CircleRepository nPlanner *nps.NotificationPlanner nRepo *nRepo.NotificationRepository tRepo *tRepo.ThingRepository } type ThingRequest struct { ID int `json:"id"` Name string `json:"name" binding:"required"` Type string `json:"type" binding:"required"` State string `json:"state"` } func NewHandler(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository, np *nps.NotificationPlanner, nRepo *nRepo.NotificationRepository, tRepo *tRepo.ThingRepository) *Handler { return &Handler{ choreRepo: cr, circleRepo: circleRepo, nPlanner: np, nRepo: nRepo, tRepo: tRepo, } } func (h *Handler) CreateThing(c *gin.Context) { log := logging.FromContext(c) currentUser, ok := auth.CurrentUser(c) if !ok { c.JSON(401, gin.H{"error": "Unauthorized"}) return } var req ThingRequest if err := c.BindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } thing := &tModel.Thing{ Name: req.Name, UserID: currentUser.ID, Type: req.Type, State: req.State, } if !isValidThingState(thing) { c.JSON(400, gin.H{"error": "Invalid state"}) return } log.Debug("Creating thing", thing) if err := h.tRepo.UpsertThing(c, thing); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(201, gin.H{ "res": thing, }) } func (h *Handler) UpdateThingState(c *gin.Context) { currentUser, ok := auth.CurrentUser(c) if !ok { c.JSON(401, gin.H{"error": "Unauthorized"}) return } thingIDRaw := c.Param("id") thingID, err := strconv.Atoi(thingIDRaw) if err != nil { c.JSON(400, gin.H{"error": "Invalid thing id"}) return } val := c.Query("value") if val == "" { c.JSON(400, gin.H{"error": "state or increment query param is required"}) return } thing, err := h.tRepo.GetThingByID(c, thingID) if thing.UserID != currentUser.ID { c.JSON(403, gin.H{"error": "Forbidden"}) return } if err != nil { c.JSON(500, gin.H{"error": "Unable to find thing"}) return } thing.State = val if !isValidThingState(thing) { c.JSON(400, gin.H{"error": "Invalid state"}) return } if err := h.tRepo.UpdateThingState(c, thing); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } shouldReturn := EvaluateTriggerAndScheduleDueDate(h, c, thing) if shouldReturn { return } c.JSON(200, gin.H{ "res": thing, }) } func EvaluateTriggerAndScheduleDueDate(h *Handler, c *gin.Context, thing *tModel.Thing) bool { thingChores, err := h.tRepo.GetThingChoresByThingId(c, thing.ID) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return true } for _, tc := range thingChores { triggered := EvaluateThingChore(tc, thing.State) if triggered { h.choreRepo.SetDueDateIfNotExisted(c, tc.ChoreID, time.Now().UTC()) } } return false } func (h *Handler) UpdateThing(c *gin.Context) { currentUser, ok := auth.CurrentUser(c) if !ok { c.JSON(401, gin.H{"error": "Unauthorized"}) return } var req ThingRequest if err := c.BindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } thing, err := h.tRepo.GetThingByID(c, req.ID) if err != nil { c.JSON(500, gin.H{"error": "Unable to find thing"}) return } if thing.UserID != currentUser.ID { c.JSON(403, gin.H{"error": "Forbidden"}) return } thing.Name = req.Name thing.Type = req.Type if req.State != "" { thing.State = req.State if !isValidThingState(thing) { c.JSON(400, gin.H{"error": "Invalid state"}) return } } if err := h.tRepo.UpsertThing(c, thing); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{ "res": thing, }) } func (h *Handler) GetAllThings(c *gin.Context) { currentUser, ok := auth.CurrentUser(c) if !ok { c.JSON(401, gin.H{"error": "Unauthorized"}) return } things, err := h.tRepo.GetUserThings(c, currentUser.ID) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{ "res": things, }) } func (h *Handler) GetThingHistory(c *gin.Context) { currentUser, ok := auth.CurrentUser(c) if !ok { c.JSON(401, gin.H{"error": "Unauthorized"}) return } thingIDRaw := c.Param("id") thingID, err := strconv.Atoi(thingIDRaw) if err != nil { c.JSON(400, gin.H{"error": "Invalid thing id"}) return } thing, err := h.tRepo.GetThingByID(c, thingID) if err != nil { c.JSON(500, gin.H{"error": "Unable to find thing"}) return } if thing.UserID != currentUser.ID { c.JSON(403, gin.H{"error": "Forbidden"}) return } offsetRaw := c.Query("offset") offset, err := strconv.Atoi(offsetRaw) if err != nil { c.JSON(400, gin.H{"error": "Invalid offset"}) return } history, err := h.tRepo.GetThingHistoryWithOffset(c, thingID, offset) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{ "res": history, }) } func (h *Handler) DeleteThing(c *gin.Context) { currentUser, ok := auth.CurrentUser(c) if !ok { c.JSON(401, gin.H{"error": "Unauthorized"}) return } thingIDRaw := c.Param("id") thingID, err := strconv.Atoi(thingIDRaw) if err != nil { c.JSON(400, gin.H{"error": "Invalid thing id"}) return } thing, err := h.tRepo.GetThingByID(c, thingID) if err != nil { c.JSON(500, gin.H{"error": "Unable to find thing"}) return } if thing.UserID != currentUser.ID { c.JSON(403, gin.H{"error": "Forbidden"}) return } // confirm there are no chores associated with the thing: thingChores, err := h.tRepo.GetThingChoresByThingId(c, thing.ID) if err != nil { c.JSON(500, gin.H{"error": "Unable to find tasks linked to this thing"}) return } if len(thingChores) > 0 { c.JSON(405, gin.H{"error": "Unable to delete thing with associated tasks"}) return } if err := h.tRepo.DeleteThing(c, thingID); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{}) } func Routes(r *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware) { thingRoutes := r.Group("things") thingRoutes.Use(auth.MiddlewareFunc()) { thingRoutes.POST("", h.CreateThing) thingRoutes.PUT("/:id/state", h.UpdateThingState) thingRoutes.PUT("", h.UpdateThing) thingRoutes.GET("", h.GetAllThings) thingRoutes.GET("/:id/history", h.GetThingHistory) thingRoutes.DELETE("/:id", h.DeleteThing) } }