diff --git a/vitty-backend-api/api/serializers/reminders.go b/vitty-backend-api/api/serializers/reminders.go new file mode 100644 index 0000000..2d8afa4 --- /dev/null +++ b/vitty-backend-api/api/serializers/reminders.go @@ -0,0 +1,18 @@ +package serializers + +import "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" + +func RemindersSerializer(reminders []models.Reminders) []map[string]interface{} { + var result []map[string]interface{} + + for _, reminder := range reminders { + out := map[string]interface{}{ + "reminder_id": reminder.ReminderId, + "reminder_name": reminder.ReminderName, + "reminder_content": reminder.ReminderContent, + "reminder_time": reminder.ReminderTime, + } + result = append(result, out) + } + return result +} diff --git a/vitty-backend-api/api/v2/initialize.go b/vitty-backend-api/api/v2/initialize.go index 2d14391..b542492 100644 --- a/vitty-backend-api/api/v2/initialize.go +++ b/vitty-backend-api/api/v2/initialize.go @@ -10,5 +10,6 @@ func V2Handler(api fiber.Router) { userHandler(group) timetableHandler(group) friendHandler(group) + reminderHandler(group) noteHandler(group) } diff --git a/vitty-backend-api/api/v2/reminderHandler.go b/vitty-backend-api/api/v2/reminderHandler.go new file mode 100644 index 0000000..20092fd --- /dev/null +++ b/vitty-backend-api/api/v2/reminderHandler.go @@ -0,0 +1,135 @@ +package v2 + +import ( + "strings" + + "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/middleware" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/serializers" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/utils" + "github.com/gofiber/fiber/v2" + "github.com/labstack/gommon/log" +) + +func reminderHandler(api fiber.Router) { + group := api.Group("/reminders") + group.Use(middleware.JWTAuthMiddleware) + group.Get("/", getReminders) + group.Post("/", createReminder) + group.Patch("/", updateReminder) + group.Delete("/:reminderId?", deleteReminder) +} + +func getReminders(c *fiber.Ctx) error { + var reminder models.Reminders + + username := c.Locals("user").(models.User).Username + reminder.UserName = username + + err, reminders := reminder.GetReminders() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Reminders not fetched", + }) + } + + if len(reminders) == 0 { + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "No reminders found", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": serializers.RemindersSerializer(reminders), + }) +} + +func createReminder(c *fiber.Ctx) error { + var reminder models.Reminders + + if err := c.BodyParser(&reminder); err != nil { + log.Error(err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body ", + }) + } + + if reminder.ReminderName == nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Reminder name is required", + }) + } + + if reminder.ReminderContent == nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Reminder content is required", + }) + } + + if !strings.Contains(reminder.ReminderId, "rem_") || len(reminder.ReminderId) < 32 { + reminder.ReminderId = utils.UUIDWithPrefix("rem") + } + + username := c.Locals("user").(models.User).Username + reminder.UserName = username + reminder.CreateReminder() + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "Reminder Saved Successfully", + }) +} + +func updateReminder(c *fiber.Ctx) error { + var reminder models.Reminders + + if err := c.BodyParser(&reminder); err != nil { + log.Error(err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body ", + }) + } + + if reminder.ReminderId == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Reminder id is required", + }) + } + + err := reminder.UpdateReminder() + if err != nil { + log.Error(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Reminder not updated", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "Reminder Saved Successfully", + }) +} + +func deleteReminder(c *fiber.Ctx) error { + var reminder models.Reminders + + reminderId := c.Params("reminderId") + + if reminderId == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Reminder id is missing", + }) + } + + reminder.ReminderId = reminderId + username := c.Locals("user").(models.User).Username + reminder.UserName = username + + err := reminder.DeleteReminder() + + if err != nil { + log.Info(err.Error()) + return c.Status(fiber.StatusBadRequest).JSON(fiber.ErrBadRequest) + } + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "Reminder deleted successfully", + }) +} diff --git a/vitty-backend-api/cmd/root.go b/vitty-backend-api/cmd/root.go index d1410b4..7ed6e65 100644 --- a/vitty-backend-api/cmd/root.go +++ b/vitty-backend-api/cmd/root.go @@ -8,7 +8,9 @@ import ( vittyCli "github.com/GDGVIT/vitty-backend/vitty-backend-api/cli" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/auth" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/jobs" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/utils" "github.com/gofiber/fiber/v2" "github.com/urfave/cli/v2" ) @@ -37,6 +39,10 @@ type Env struct { google_client_id string google_client_secret string google_redirect_uri string + + // Job Variable + isRunJobs string + jobTime jobs.JobTime } // Method to create a new VittyApp @@ -55,6 +61,11 @@ func (v *VittyApp) setEnv() { v.env.google_client_id = os.Getenv("GOOGLE_CLIENT_ID") v.env.google_client_secret = os.Getenv("GOOGLE_CLIENT_SECRET") v.env.google_redirect_uri = os.Getenv("GOOGLE_REDIRECT_URI") + + //Jobs + v.env.isRunJobs = os.Getenv("RUN_JOBS") + v.env.jobTime = utils.ParseJobTimes() + } // Method to initialize CLI app @@ -94,6 +105,9 @@ func (v *VittyApp) init() { auth.InitializeGoogleOauth(v.env.google_client_id, v.env.google_client_secret, v.env.google_redirect_uri) auth.InitializeFirebaseApp() + // Initialize jobs + jobs.InitializeJobs(v.env.isRunJobs, v.env.debug, v.env.jobTime) + // Initialize Web app v.initWebApp() diff --git a/vitty-backend-api/example.env/example.local b/vitty-backend-api/example.env/example.local index a442b56..3820765 100644 --- a/vitty-backend-api/example.env/example.local +++ b/vitty-backend-api/example.env/example.local @@ -15,4 +15,13 @@ POSTGRES_PORT=5432 # Auth Variables # --------------------------------------------------------------- OAUTH_CALLBACK_URL=http://localhost:3000/auth/callback -JWT_SECRET=secret \ No newline at end of file +JWT_SECRET=secret + +# Job Variables +# --------------------------------------------------------------- +RUN_JOBS=false +#Works only if debug is true +DAILY_JOB_MIN=0 +DAILY_JOB_SEC=10 +WEEKLY_JOB_MIN=0 +WEEKLY_JOB_SEC=20 \ No newline at end of file diff --git a/vitty-backend-api/example.env/example.production b/vitty-backend-api/example.env/example.production index 3a60c3a..1e832d4 100644 --- a/vitty-backend-api/example.env/example.production +++ b/vitty-backend-api/example.env/example.production @@ -15,4 +15,6 @@ POSTGRES_PORT=5432 # Auth Variables # --------------------------------------------------------------- OAUTH_CALLBACK_URL=https:///auth/callback -JWT_SECRET=secret \ No newline at end of file +JWT_SECRET=secret +# --------------------------------------------------------------- +RUN_JOBS=false \ No newline at end of file diff --git a/vitty-backend-api/internal/jobs/jobs.go b/vitty-backend-api/internal/jobs/jobs.go new file mode 100644 index 0000000..c93c717 --- /dev/null +++ b/vitty-backend-api/internal/jobs/jobs.go @@ -0,0 +1,60 @@ +package jobs + +import ( + "log" + "time" + + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" +) + +type JobTime struct { + DAILY_JOB_MIN int + DAILY_JOB_SEC int + WEEKLY_JOB_MIN int + WEEKLY_JOB_SEC int +} + +func InitializeJobs(isRunJobs string, debug string, jobTimes JobTime) { + if isRunJobs == "true" { + if debug == "true" { + + timeDailyJob := time.Duration(jobTimes.DAILY_JOB_MIN)*time.Minute + time.Duration(jobTimes.DAILY_JOB_SEC)*time.Second + timeWeeklyJob := time.Duration(jobTimes.WEEKLY_JOB_MIN)*time.Minute + time.Duration(jobTimes.WEEKLY_JOB_SEC)*time.Second + go startDailyJob(timeDailyJob) + go startWeeklyJob(timeWeeklyJob) + } else { + go startDailyJob(24 * time.Hour) + go startWeeklyJob(7 * 24 * time.Hour) + } + log.Println("Jobs are running") + } else { + log.Println("Jobs disabled") + } + +} + +var reminder models.Reminders + +func startDailyJob(jobTime time.Duration) { + ticker := time.NewTicker(jobTime) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + reminder.SoftDeleteExpiredReminders() + } + } +} + +func startWeeklyJob(jobTime time.Duration) { + ticker := time.NewTicker(jobTime) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + reminder.CleanupOldReminders() + } + } +} diff --git a/vitty-backend-api/internal/models/initialize.go b/vitty-backend-api/internal/models/initialize.go index 79130e3..a6df1bd 100644 --- a/vitty-backend-api/internal/models/initialize.go +++ b/vitty-backend-api/internal/models/initialize.go @@ -11,6 +11,7 @@ func InitializeModels() { "User": &User{}, "Timetable": &Timetable{}, "Friend Requests": &FriendRequest{}, + "Reminders": &Reminders{}, "Notes": &Notes{}, "Courses": &Courses{}, } diff --git a/vitty-backend-api/internal/models/reminders.go b/vitty-backend-api/internal/models/reminders.go new file mode 100644 index 0000000..e5b8158 --- /dev/null +++ b/vitty-backend-api/internal/models/reminders.go @@ -0,0 +1,68 @@ +package models + +import ( + "time" + + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" + "gorm.io/gorm" + "gorm.io/plugin/soft_delete" +) + +type Reminders struct { + ReminderId string `json:"reminder_id,omitempty" gorm:"unique"` + ReminderName *string `json:"reminder_name" gorm:"primaryKey"` + UserName string `json:"user_name" gorm:"primaryKey;foreignKey:Username;constraint:OnDelete:CASCADE"` + ReminderContent *string `json:"reminder_content"` + ReminderTime *time.Time `json:"reminder_time,omitempty"` + User User `gorm:"foreignKey:UserName;references:Username;constraint:OnDelete:CASCADE"` + DeletedAt soft_delete.DeletedAt `json:"-,omitempty" gorm:"index"` +} + +func (r *Reminders) CreateReminder() error { + err := database.DB.Create(&r).Error + return err +} + +func (r *Reminders) GetReminders() (error, []Reminders) { + var reminders []Reminders + err := database.DB.Where(&r).Find(&reminders).Error + return err, reminders +} + +func (r *Reminders) UpdateReminder() error { + updateInteface := make(map[string]interface{}) + + if r.ReminderName != nil { + updateInteface["reminder_name"] = *r.ReminderName + } + + if r.ReminderContent != nil { + updateInteface["reminder_content"] = *r.ReminderContent + } + + if r.ReminderTime != nil { + updateInteface["reminder_time"] = *r.ReminderTime + } + + err := database.DB.Model(&Reminders{}).Where("reminder_id = ?", r.ReminderId).UpdateColumns(updateInteface).Error + + return err +} + +func (r *Reminders) DeleteReminder() error { + result := database.DB.Where("reminder_id = ? AND username like ?", r.ReminderId, r.UserName).Delete(&r) + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + + return result.Error +} + +func (r *Reminders) SoftDeleteExpiredReminders() { + now := time.Now() + database.DB.Model(&Reminders{}).Where("reminder_time < ?", now).Delete(&Reminders{}) +} + +func (r *Reminders) CleanupOldReminders() { + database.DB.Unscoped().Where("deleted_at < NOW() - INTERVAL '7 days'").Delete(&Reminders{}) +} diff --git a/vitty-backend-api/internal/utils/jobs.go b/vitty-backend-api/internal/utils/jobs.go new file mode 100644 index 0000000..7ddec52 --- /dev/null +++ b/vitty-backend-api/internal/utils/jobs.go @@ -0,0 +1,41 @@ +package utils + +import ( + "log" + "os" + "strconv" + + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/jobs" +) + +func ParseJobTimes() jobs.JobTime { + var err error + var jobTime jobs.JobTime + + jobTime.DAILY_JOB_MIN, err = strconv.Atoi(os.Getenv("DAILY_JOB_MIN")) + + if err != nil { + log.Fatal(err, "JOB_DAILY_MIN_PARSE_ERR") + } + + jobTime.DAILY_JOB_SEC, err = strconv.Atoi(os.Getenv("DAILY_JOB_SEC")) + + if err != nil { + log.Fatal(err, "JOB_DAILY_SEC_PARSE_ERR") + } + + jobTime.WEEKLY_JOB_MIN, err = strconv.Atoi(os.Getenv("WEEKLY_JOB_MIN")) + + if err != nil { + log.Fatal(err, "JOB_WEEKLY_MIN_PARSE_ERR") + } + + jobTime.WEEKLY_JOB_SEC, err = strconv.Atoi(os.Getenv("WEEKLY_JOB_SEC")) + + if err != nil { + log.Fatal(err, "JOB_WEEKLY_SEC_PARSE_ERR") + } + + log.Println("Parse Done") + return jobTime +}