package main import ( "embed" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/joho/godotenv" _ "github.com/lib/pq" "holiday-api/holiday" "holiday-api/migration" "holiday-api/webhook" "html/template" "log" "net/http" "os" "strconv" "strings" "time" ) //go:embed db/dev/*.sql var devMigrations embed.FS //go:embed db/prod/*.sql var prodMigrations embed.FS var isDev = false func init() { godotenv.Load() if strings.Contains(os.Getenv("PROFILE"), "dev") { isDev = true } log.SetPrefix("") log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) } func main() { client, err := connectToDb() if err != nil { log.Fatalf("couldn't connect to db: %v", err) } migrationFolder := prodMigrations if isDev { migrationFolder = devMigrations } if err := migration.InitializeMigrations(client, migrationFolder); err != nil { log.Fatalf("couldn't execute migrations: %v", err) } g := gin.Default() g.Static("assets", "assets") loadTemplates(g) holidayService := holiday.HolidayService{DB: client} countryService := holiday.CountryService{DB: client} yearService := holiday.YearService{DB: client} webhookService := webhook.WebhookService{ DB: client, Events: make(chan webhook.Event, 10), Authorization: "TODO", // TODO add authorization fetching } g.GET("/api/v1/holidays", getHolidays(holidayService)) setupAdminDashboard(g.Group("/admin"), holidayService, countryService, yearService, webhookService) g.GET("/", func(c *gin.Context) { year := time.Now().Year() search := holiday.Search{Country: "HR", Year: &year} if err := c.ShouldBindQuery(&search); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } holidays, _ := holidayService.Find(search, holiday.Paging{PageSize: 100}) countries, _ := countryService.Find() years, _ := yearService.Find() c.HTML(http.StatusOK, "index.gohtml", gin.H{"Years": years, "Countries": countries, "Search": search, "Holidays": mapHolidays(holidays).Holidays}) }) g.GET("/documentation", func(c *gin.Context) { countries, _ := countryService.Find() years, _ := yearService.Find() c.HTML(http.StatusOK, "documentation.gohtml", gin.H{"Years": years, "Countries": countries}) }) g.GET("/search", func(c *gin.Context) { request := holiday.Search{} if err := c.ShouldBindQuery(&request); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } search := holiday.Search{Country: request.Country, Date: request.Date} holidays, _ := holidayService.Find(search, holiday.Paging{PageSize: 100}) countries, _ := countryService.Find() c.HTML(http.StatusOK, "search.gohtml", gin.H{"Countries": countries, "Search": search, "Holidays": mapHolidays(holidays).Holidays}) }) g.GET("/dialogs/check-is-a-holiday", func(c *gin.Context) { countries, _ := countryService.Find() c.HTML(http.StatusOK, "check-is-a-holiday.gohtml", gin.H{"Countries": countries}) }) log.Fatal(http.ListenAndServe(":5281", g)) } func loadTemplates(g *gin.Engine) { g.SetFuncMap(template.FuncMap{ "boolcmp": func(value *bool, expected string) bool { if value == nil { return expected == "nil" } else { return (*value && expected == "true") || (!(*value) && expected == "false") } }, "deferint": func(value *int) int { return *value }, "intpeq": func(selected *int, value int) bool { if selected != nil { return *selected == value } return false }, }) g.LoadHTMLFiles( "templates/index.gohtml", "templates/search.gohtml", "templates/documentation.gohtml", "templates/admin_dashboard.gohtml", "templates/countries.gohtml", "templates/webhooks.gohtml", "templates/jobs.gohtml", "templates/dialogs/add-holiday.gohtml", "templates/dialogs/edit-holiday.gohtml", "templates/dialogs/delete-holiday.gohtml", "templates/dialogs/check-is-a-holiday.gohtml", "templates/dialogs/add-webhook.gohtml", "templates/dialogs/edit-webhook.gohtml", "templates/dialogs/delete-webhook.gohtml", "templates/dialogs/edit-country.gohtml", "templates/dialogs/delete-country.gohtml", ) } func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService, webhookService webhook.WebhookService) { adminDashboard.Use(gin.BasicAuth(loadAuth())) adminDashboard.GET("/", func(c *gin.Context) { search := holiday.Search{Country: "HR", Year: new(int)} *search.Year = time.Now().Year() if err := c.ShouldBindQuery(&search); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } holidays, _ := service.Find(search, holiday.Paging{PageSize: 100}) holidayResponse := mapHolidays(holidays) countries, _ := countryService.Find() years, _ := yearService.Find() response := map[string]any{} response["Holidays"] = holidayResponse response["Search"] = search response["Countries"] = countries response["Years"] = years c.HTML(http.StatusOK, "admin_dashboard.gohtml", response) }) adminDashboard.POST("/holidays", func(c *gin.Context) { request := struct { Id *string `form:"id"` Name string `form:"name" binding:"required,min=1"` Description string `form:"description"` IsStateHoliday bool `form:"state_holiday"` IsReligiousHoliday bool `form:"religious_holiday"` Country string `form:"country" binding:"len=2"` Date time.Time `form:"date" time_format:"2006-01-02"` }{} if err := c.ShouldBind(&request); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } hol := holiday.Holiday{ Country: request.Country, Date: request.Date, Name: request.Name, Description: request.Description, IsStateHoliday: request.IsStateHoliday, IsReligiousHoliday: request.IsReligiousHoliday, } var err error if request.Id != nil { hol.Id = uuid.MustParse(*request.Id) hol, err = service.Update(hol) } else { hol, err = service.Create(hol) } if err != nil { c.AbortWithError(http.StatusInternalServerError, err) } else { c.Redirect(http.StatusSeeOther, "/admin?country="+request.Country+"&year="+strconv.FormatInt(int64(request.Date.Year()), 10)) } }) adminDashboard.POST("/holidays/:id/delete", func(c *gin.Context) { id := uuid.MustParse(c.Param("id")) hol, err := service.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } if err := service.Delete(id); err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.Redirect(http.StatusSeeOther, "/admin?country="+hol.Country+"&year="+strconv.FormatInt(int64(hol.Date.Year()), 10)) }) adminDashboard.GET("/countries", func(c *gin.Context) { countries, _ := countryService.Find() c.HTML(http.StatusOK, "countries.gohtml", gin.H{"Countries": countries}) }) adminDashboard.POST("/countries", func(c *gin.Context) { request := struct { Id *string `form:"id"` IsoName string `form:"iso_name" binding:"required,min=2,max=2"` Name string `form:"name" binding:"required,min=1,max=45"` }{} if err := c.ShouldBind(&request); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } country := holiday.Country{ IsoName: request.IsoName, Name: request.Name, } var err error if request.Id != nil { country.Id = uuid.MustParse(*request.Id) country, err = countryService.Update(country) } else { country, err = countryService.Create(country) } if err != nil { c.AbortWithError(http.StatusInternalServerError, err) } else { c.Redirect(http.StatusSeeOther, "/admin/countries") } }) adminDashboard.POST("/countries/:id/delete", func(c *gin.Context) { id := uuid.MustParse(c.Param("id")) _, err := countryService.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } if err := countryService.Delete(id); err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.Redirect(http.StatusSeeOther, "/admin/countries") }) adminDashboard.GET("/webhooks", func(c *gin.Context) { webhooks, _ := webhookService.Find() c.HTML(http.StatusOK, "webhooks.gohtml", gin.H{"Webhooks": webhooks}) }) adminDashboard.GET("/jobs", func(c *gin.Context) { jobs, _ := webhookService.FindAllJobs() c.HTML(http.StatusOK, "jobs.gohtml", gin.H{"Jobs": jobs}) }) adminDashboard.POST("/webhooks", func(c *gin.Context) { request := struct { Id *string `form:"id"` Url string `form:"url" binding:"required,min=1"` RetryCount int `form:"retry_count" binding:"required,min=1,max=20"` Country string `form:"country" binding:"len=2"` OnCreated bool `form:"on_created"` OnEdited bool `form:"on_edited"` OnDeleted bool `form:"on_deleted"` }{} if err := c.ShouldBind(&request); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } hook := webhook.Webhook{ Url: request.Url, Country: request.Country, RetryCount: request.RetryCount, OnCreated: request.OnCreated, OnEdited: request.OnEdited, OnDeleted: request.OnDeleted, } var err error if request.Id != nil { hook.Id = uuid.MustParse(*request.Id) hook, err = webhookService.Update(hook) } else { hook.Id = uuid.Must(uuid.NewRandom()) hook.Created = time.Now() hook, err = webhookService.Create(hook) } if err != nil { c.AbortWithError(http.StatusInternalServerError, err) } else { c.Redirect(http.StatusSeeOther, "/admin/webhooks") } }) adminDashboard.POST("/webhooks/:id/delete", func(c *gin.Context) { id := uuid.MustParse(c.Param("id")) _, err := webhookService.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } if err := webhookService.Delete(id); err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.Redirect(http.StatusSeeOther, "/admin/webhooks") }) adminDashboard.GET("/dialogs/add-holiday", func(c *gin.Context) { countries, _ := countryService.Find() c.HTML(http.StatusOK, "add-holiday.gohtml", gin.H{"Countries": countries}) }) adminDashboard.GET("/dialogs/add-webhook", func(c *gin.Context) { countries, _ := countryService.Find() c.HTML(http.StatusOK, "add-webhook.gohtml", gin.H{"Countries": countries}) }) adminDashboard.GET("/dialogs/edit-holiday", func(c *gin.Context) { id := uuid.MustParse(c.Query("id")) hol, err := service.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } countries, _ := countryService.Find() c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Countries": countries, "Holiday": hol}) }) adminDashboard.GET("/dialogs/edit-webhook", func(c *gin.Context) { id := uuid.MustParse(c.Query("id")) hook, err := webhookService.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } countries, _ := countryService.Find() c.HTML(http.StatusOK, "edit-webhook.gohtml", gin.H{"Countries": countries, "Webhook": hook}) }) adminDashboard.GET("/dialogs/edit-country", func(c *gin.Context) { id := uuid.MustParse(c.Query("id")) country, err := countryService.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } c.HTML(http.StatusOK, "edit-country.gohtml", gin.H{"Country": country}) }) adminDashboard.GET("/dialogs/delete-holiday", func(c *gin.Context) { id := uuid.MustParse(c.Query("id")) hol, err := service.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } c.HTML(http.StatusOK, "delete-holiday.gohtml", gin.H{"Holiday": hol}) }) adminDashboard.GET("/dialogs/delete-country", func(c *gin.Context) { id := uuid.MustParse(c.Query("id")) country, err := countryService.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } c.HTML(http.StatusOK, "delete-country.gohtml", gin.H{"Country": country}) }) adminDashboard.GET("/dialogs/delete-webhook", func(c *gin.Context) { id := uuid.MustParse(c.Query("id")) hook, err := webhookService.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } c.HTML(http.StatusOK, "delete-webhook.gohtml", gin.H{"Webhook": hook}) }) } func loadAuth() map[string]string { credentials := envMustExist("AUTH_KEY") values := strings.Split(credentials, ":") return map[string]string{values[0]: values[1]} }