diff --git a/main.go b/main.go index d3a9bfc..edb9ecb 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "github.com/todanni/api/service/auth" "github.com/todanni/api/service/dashboard" "github.com/todanni/api/service/project" + "github.com/todanni/api/service/routine" "github.com/todanni/api/service/task" "github.com/todanni/api/token" ) @@ -34,7 +35,13 @@ func main() { } // Perform migrations - err = db.AutoMigrate(&models.User{}, &models.Dashboard{}, &models.Project{}, &models.Task{}) + err = db.AutoMigrate( + &models.User{}, + &models.Dashboard{}, + &models.Project{}, + &models.Task{}, + &models.Routine{}, + &models.RoutineRecord{}) if err != nil { log.Fatalf("couldn't auto migrate: %v", err) } @@ -50,12 +57,14 @@ func main() { projectRepo := repository.NewProjectRepository(db) taskRepo := repository.NewTaskRepository(db) dashboardRepo := repository.NewDashboardRepository(db) + routineRepo := repository.NewRoutineRepository(db) // Initialise services project.NewProjectService(r, *authMiddleware, projectRepo) task.NewTaskService(r, taskRepo, *authMiddleware) dashboard.NewDashboardService(r, dashboardRepo) auth.NewAuthService(r, cfg, userRepo, dashboardRepo, projectRepo, *authMiddleware) + routine.NewRoutineService(r, *authMiddleware, routineRepo) // Start the servers and listen log.Fatal(http.ListenAndServe(":8083", r)) diff --git a/models/routine.go b/models/routine.go new file mode 100644 index 0000000..1b66f59 --- /dev/null +++ b/models/routine.go @@ -0,0 +1,19 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type Routine struct { + gorm.Model + Name string `json:"name"` + Days string `json:"days"` + UserID string `json:"-"` +} + +type RoutineRecord struct { + ID uint `json:"id"` + Timestamp time.Time `json:"timestamp"` +} diff --git a/repository/routine_repo.go b/repository/routine_repo.go new file mode 100644 index 0000000..f6d45a0 --- /dev/null +++ b/repository/routine_repo.go @@ -0,0 +1,65 @@ +package repository + +import ( + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "github.com/todanni/api/models" +) + +type RoutineRepository interface { + CreateRoutine(routine models.Routine) (models.Routine, error) + UpdateRoutine(routine models.Routine) (models.Routine, error) + ListRoutinesByUser(userID string) ([]models.Routine, error) + GetRoutineByID(routineID string) (models.Routine, error) + DeleteRoutine(routineID string) error + CreateRoutineRecord(record models.RoutineRecord) (models.RoutineRecord, error) + DeleteRoutineRecord(record models.RoutineRecord) error +} + +type routineRepo struct { + db *gorm.DB +} + +func (r *routineRepo) CreateRoutineRecord(record models.RoutineRecord) (models.RoutineRecord, error) { + result := r.db.Create(&record) + return record, result.Error +} + +func (r *routineRepo) DeleteRoutineRecord(record models.RoutineRecord) error { + result := r.db.Delete(record) + return result.Error +} + +func (r *routineRepo) GetRoutineByID(routineID string) (models.Routine, error) { + var routine models.Routine + result := r.db.First(&routine, routineID) + return routine, result.Error +} + +func (r *routineRepo) DeleteRoutine(routineID string) error { + result := r.db.Delete(&models.Routine{}, routineID) + return result.Error +} + +func (r *routineRepo) ListRoutinesByUser(userID string) ([]models.Routine, error) { + var routines []models.Routine + result := r.db.Where("user_id = ?", userID).Find(&routines) + return routines, result.Error +} + +func (r *routineRepo) CreateRoutine(routine models.Routine) (models.Routine, error) { + result := r.db.Create(&routine) + return routine, result.Error +} + +func (r *routineRepo) UpdateRoutine(routine models.Routine) (models.Routine, error) { + result := r.db.Model(&routine).Clauses(clause.Returning{}).Updates(routine) + return routine, result.Error +} + +func NewRoutineRepository(db *gorm.DB) RoutineRepository { + return &routineRepo{ + db: db, + } +} diff --git a/service/routine/request.go b/service/routine/request.go new file mode 100644 index 0000000..ba0e256 --- /dev/null +++ b/service/routine/request.go @@ -0,0 +1,31 @@ +package routine + +import ( + "time" +) + +type CreateRoutineRequest struct { + Name string `json:"name"` + Days string `json:"days"` +} + +type CreateRoutineResponse struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + Days string `json:"days"` +} + +type ListRoutinesResponse struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + Days string `json:"days"` +} + +type UpdateRoutineRequest struct { + Name string `json:"name"` + Days string `json:"days"` +} diff --git a/service/routine/routes.go b/service/routine/routes.go new file mode 100644 index 0000000..c37710e --- /dev/null +++ b/service/routine/routes.go @@ -0,0 +1,18 @@ +package routine + +import "net/http" + +const ( + APIPath = "/routines" +) + +func (s *routineService) routes() { + r := s.router.PathPrefix(APIPath).Subrouter() + r.Use(s.middleware.JwtMiddleware) + + r.HandleFunc("/", s.ListRoutinesHandler).Methods(http.MethodGet) + r.HandleFunc("/", s.CreateRoutineHandler).Methods(http.MethodPost) + r.HandleFunc("/{id}", s.GetRoutineHandler).Methods(http.MethodGet) + r.HandleFunc("/{id}", s.UpdateRoutineHandler).Methods(http.MethodPatch) + r.HandleFunc("/{id}", s.DeleteRoutineHandler).Methods(http.MethodDelete) +} diff --git a/service/routine/service.go b/service/routine/service.go new file mode 100644 index 0000000..0a75ef0 --- /dev/null +++ b/service/routine/service.go @@ -0,0 +1,277 @@ +package routine + +import ( + "encoding/json" + "net/http" + "strconv" + + validation "github.com/go-ozzo/ozzo-validation" + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" + + "github.com/todanni/api/models" + "github.com/todanni/api/repository" + "github.com/todanni/api/token" +) + +type RoutinesService interface { + CreateRoutineHandler(w http.ResponseWriter, r *http.Request) + GetRoutineHandler(w http.ResponseWriter, r *http.Request) + UpdateRoutineHandler(w http.ResponseWriter, r *http.Request) + ListRoutinesHandler(w http.ResponseWriter, r *http.Request) + DeleteRoutineHandler(w http.ResponseWriter, r *http.Request) + + SaveRoutineRecordHandler(w http.ResponseWriter, r *http.Request) + DeleteRoutineRecordHandler(w http.ResponseWriter, r *http.Request) +} + +type routineService struct { + router *mux.Router + repo repository.RoutineRepository + middleware token.AuthMiddleware +} + +func (s *routineService) SaveRoutineRecordHandler(w http.ResponseWriter, r *http.Request) { + accessToken := r.Context().Value(token.AccessTokenContextKey).(*token.ToDanniToken) + + userID := accessToken.GetUserID() + if userID == "" { + http.Error(w, "invalid user ID in token", http.StatusUnauthorized) + return + } +} + +func (s *routineService) DeleteRoutineRecordHandler(w http.ResponseWriter, r *http.Request) { + accessToken := r.Context().Value(token.AccessTokenContextKey).(*token.ToDanniToken) + + userID := accessToken.GetUserID() + if userID == "" { + http.Error(w, "invalid user ID in token", http.StatusUnauthorized) + return + } +} + +func NewRoutineService(router *mux.Router, mw token.AuthMiddleware, repo repository.RoutineRepository) RoutinesService { + service := &routineService{ + router: router, + repo: repo, + middleware: mw, + } + service.routes() + return service +} + +func (s *routineService) ListRoutinesHandler(w http.ResponseWriter, r *http.Request) { + accessToken := r.Context().Value(token.AccessTokenContextKey).(*token.ToDanniToken) + + userID := accessToken.GetUserID() + if userID == "" { + http.Error(w, "invalid user ID in token", http.StatusUnauthorized) + return + } + + routines, err := s.repo.ListRoutinesByUser(userID) + if err != nil { + http.Error(w, "couldn't retrieve routines", http.StatusInternalServerError) + return + } + + var response []ListRoutinesResponse + for _, routine := range routines { + response = append(response, ListRoutinesResponse{ + ID: routine.ID, + Name: routine.Name, + Days: routine.Days, + CreatedAt: routine.CreatedAt, + UpdatedAt: routine.UpdatedAt, + }) + } + + responseBody, err := json.Marshal(response) + if err != nil { + http.Error(w, "couldn't marshall body", http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "application/json") + w.Write(responseBody) +} + +func (s *routineService) CreateRoutineHandler(w http.ResponseWriter, r *http.Request) { + accessToken := r.Context().Value(token.AccessTokenContextKey).(*token.ToDanniToken) + + userID := accessToken.GetUserID() + if userID == "" { + http.Error(w, "invalid user ID in token", http.StatusUnauthorized) + return + } + + var createRequest CreateRoutineRequest + err := json.NewDecoder(r.Body).Decode(&createRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err = validation.ValidateStruct(&createRequest, + validation.Field(&createRequest.Name, validation.Required), + ); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + routine, err := s.repo.CreateRoutine(models.Routine{ + Name: createRequest.Name, + Days: createRequest.Days, + }) + if err != nil { + http.Error(w, "couldn't create routine", http.StatusInternalServerError) + return + } + + response := CreateRoutineResponse{ + ID: routine.ID, + CreatedAt: routine.CreatedAt, + UpdatedAt: routine.UpdatedAt, + Name: routine.Name, + Days: createRequest.Days, + } + + responseBody, err := json.Marshal(response) + if err != nil { + http.Error(w, "couldn't marshall body", http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "application/json") + w.Write(responseBody) +} + +func (s *routineService) GetRoutineHandler(w http.ResponseWriter, r *http.Request) { + // Get the routine ID from the request + params := mux.Vars(r) + routineID := params["id"] + + accessToken := r.Context().Value(token.AccessTokenContextKey).(*token.ToDanniToken) + userID := accessToken.GetUserID() + if userID == "" { + http.Error(w, "invalid user ID in token", http.StatusUnauthorized) + return + } + + routine, err := s.repo.GetRoutineByID(routineID) + if err != nil { + log.Error(err) + http.Error(w, "couldn't find routine", http.StatusNotFound) + return + } + + // We don't want to reveal to users whether a routine exists + // if they are not the user that created it + if routine.UserID != userID { + http.Error(w, "routine not found", http.StatusNotFound) + return + } + + responseBody, err := json.Marshal(routine) + if err != nil { + http.Error(w, "couldn't marshall body", http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "application/json") + w.Write(responseBody) +} + +func (s *routineService) DeleteRoutineHandler(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + routineID := params["id"] + + accessToken := r.Context().Value(token.AccessTokenContextKey).(*token.ToDanniToken) + userID := accessToken.GetUserID() + if userID == "" { + http.Error(w, "invalid user ID in token", http.StatusUnauthorized) + return + } + + routine, err := s.repo.GetRoutineByID(routineID) + if err != nil { + log.Error(err) + http.Error(w, "couldn't find routine", http.StatusNotFound) + return + } + + if routine.UserID != userID { + http.Error(w, "only the routine owner can delete a routine", http.StatusForbidden) + return + } + + err = s.repo.DeleteRoutine(routineID) + if err != nil { + log.Error(err) + http.Error(w, "couldn't delete routine", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +func (s *routineService) UpdateRoutineHandler(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + routineIDStr := params["routine_id"] + + accessToken := r.Context().Value(token.AccessTokenContextKey).(*token.ToDanniToken) + userID := accessToken.GetUserID() + if userID == "" { + http.Error(w, "invalid user ID in token", http.StatusUnauthorized) + return + } + + routine, err := s.repo.GetRoutineByID(routineIDStr) + if err != nil { + log.Error(err) + http.Error(w, "couldn't find routine", http.StatusNotFound) + return + } + + if routine.UserID != userID { + http.Error(w, "only the routine owner can update a routine", http.StatusForbidden) + return + } + + var updateRequest UpdateRoutineRequest + err = json.NewDecoder(r.Body).Decode(&updateRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + routineID, err := strconv.ParseUint(routineIDStr, 10, 32) + if err != nil { + log.Error(err) + http.Error(w, "invalid member ID", http.StatusBadRequest) + return + } + + updatedRoutine, err := s.repo.UpdateRoutine(models.Routine{ + Model: gorm.Model{ + ID: uint(routineID), + }, + Name: updateRequest.Name, + Days: updateRequest.Days, + }) + + if err != nil { + log.Error(err) + http.Error(w, "couldn't update routine", http.StatusInternalServerError) + return + } + + responseBody, err := json.Marshal(updatedRoutine) + if err != nil { + http.Error(w, "couldn't marshall body", http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "application/json") + w.Write(responseBody) +}