2023-06-16 07:42:50 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"embed"
|
2023-06-23 17:31:02 +00:00
|
|
|
"github.com/gin-gonic/gin"
|
2023-06-20 14:10:46 +00:00
|
|
|
"github.com/google/uuid"
|
2023-06-16 07:42:50 +00:00
|
|
|
"github.com/joho/godotenv"
|
2023-06-20 14:10:46 +00:00
|
|
|
_ "github.com/lib/pq"
|
|
|
|
"holiday-api/holiday"
|
|
|
|
"holiday-api/migration"
|
|
|
|
"html/template"
|
2023-06-16 07:42:50 +00:00
|
|
|
"log"
|
|
|
|
"net/http"
|
2023-06-20 14:10:46 +00:00
|
|
|
"os"
|
2023-08-05 15:50:40 +00:00
|
|
|
"strconv"
|
2023-06-20 14:10:46 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
2023-06-16 07:42:50 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
//go:embed db/dev/*.sql
|
|
|
|
var devMigrations embed.FS
|
|
|
|
|
2023-06-20 14:10:46 +00:00
|
|
|
//go:embed db/prod/*.sql
|
|
|
|
var prodMigrations embed.FS
|
|
|
|
|
|
|
|
var isDev = false
|
|
|
|
|
2023-06-16 07:42:50 +00:00
|
|
|
func init() {
|
|
|
|
godotenv.Load()
|
2023-06-20 14:10:46 +00:00
|
|
|
if strings.Contains(os.Getenv("PROFILE"), "dev") {
|
|
|
|
isDev = true
|
|
|
|
}
|
2023-06-16 07:42:50 +00:00
|
|
|
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)
|
|
|
|
}
|
2023-06-20 14:10:46 +00:00
|
|
|
migrationFolder := prodMigrations
|
|
|
|
if isDev {
|
|
|
|
migrationFolder = devMigrations
|
|
|
|
}
|
|
|
|
if err := migration.InitializeMigrations(client, migrationFolder); err != nil {
|
2023-06-16 07:42:50 +00:00
|
|
|
log.Fatalf("couldn't execute migrations: %v", err)
|
|
|
|
}
|
|
|
|
|
2023-06-23 17:31:02 +00:00
|
|
|
g := gin.Default()
|
2023-06-16 07:42:50 +00:00
|
|
|
|
2023-08-05 15:50:40 +00:00
|
|
|
g.Static("assets", "assets")
|
|
|
|
|
2023-06-23 17:31:02 +00:00
|
|
|
loadTemplates(g)
|
2023-06-20 14:10:46 +00:00
|
|
|
|
2023-08-05 19:34:09 +00:00
|
|
|
holidayService := holiday.HolidayService{DB: client}
|
|
|
|
countryService := holiday.CountryService{DB: client}
|
2023-08-05 19:49:33 +00:00
|
|
|
yearService := holiday.YearService{DB: client}
|
2023-06-20 14:10:46 +00:00
|
|
|
|
2023-06-23 17:31:02 +00:00
|
|
|
g.GET("/api/v1/holidays", getHolidays(holidayService))
|
2023-06-20 16:00:12 +00:00
|
|
|
|
2023-08-05 19:49:33 +00:00
|
|
|
setupAdminDashboard(g.Group("/admin"), holidayService, countryService, yearService)
|
2023-06-20 16:00:12 +00:00
|
|
|
|
2023-06-23 17:31:02 +00:00
|
|
|
g.GET("/", func(c *gin.Context) {
|
2023-12-31 12:35:29 +00:00
|
|
|
search := holiday.Search{Country: "HR", Year: nil}
|
2023-08-05 15:50:40 +00:00
|
|
|
if err := c.ShouldBindQuery(&search); err != nil {
|
2023-06-23 17:31:02 +00:00
|
|
|
c.AbortWithError(http.StatusBadRequest, err)
|
|
|
|
return
|
2023-06-20 16:00:12 +00:00
|
|
|
}
|
2023-08-05 19:49:33 +00:00
|
|
|
years, _ := yearService.Find()
|
2023-12-31 12:35:29 +00:00
|
|
|
if search.Year == nil {
|
2024-01-07 21:01:58 +00:00
|
|
|
c.HTML(http.StatusOK, "index.gohtml", gin.H{"Year": time.Now().Year(), "Years": years})
|
2023-12-31 12:35:29 +00:00
|
|
|
return
|
|
|
|
}
|
2024-01-07 21:01:58 +00:00
|
|
|
countries, _ := countryService.Find()
|
2023-12-31 12:35:29 +00:00
|
|
|
holidays, _ := holidayService.Find(search, holiday.Paging{PageSize: 100})
|
|
|
|
c.HTML(http.StatusOK, "results.gohtml", gin.H{"Years": years, "Countries": countries, "Search": search, "Holidays": mapHolidays(holidays).Holidays})
|
2023-06-20 16:00:12 +00:00
|
|
|
})
|
2023-08-05 15:50:40 +00:00
|
|
|
g.GET("/documentation", func(c *gin.Context) {
|
2023-08-05 19:34:09 +00:00
|
|
|
countries, _ := countryService.Find()
|
2023-08-05 19:49:33 +00:00
|
|
|
years, _ := yearService.Find()
|
2024-01-07 21:01:58 +00:00
|
|
|
c.HTML(http.StatusOK, "documentation.gohtml", gin.H{"Year": time.Now().Year(), "Years": years, "Countries": countries})
|
2023-08-05 15:50:40 +00:00
|
|
|
})
|
2023-06-23 17:31:02 +00:00
|
|
|
log.Fatal(http.ListenAndServe(":5281", g))
|
2023-06-16 07:42:50 +00:00
|
|
|
}
|
|
|
|
|
2023-06-23 17:31:02 +00:00
|
|
|
func loadTemplates(g *gin.Engine) {
|
|
|
|
g.SetFuncMap(template.FuncMap{
|
2023-06-20 14:10:46 +00:00
|
|
|
"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 },
|
2023-08-05 15:50:40 +00:00
|
|
|
"intpeq": func(selected *int, value int) bool {
|
|
|
|
if selected != nil {
|
|
|
|
return *selected == value
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
},
|
2023-12-31 12:35:29 +00:00
|
|
|
"importSvg": IncludeHTML,
|
2023-06-23 17:31:02 +00:00
|
|
|
})
|
2023-08-05 15:50:40 +00:00
|
|
|
g.LoadHTMLFiles(
|
|
|
|
"templates/index.gohtml",
|
2023-12-31 12:35:29 +00:00
|
|
|
"templates/results.gohtml",
|
2023-08-05 15:50:40 +00:00
|
|
|
"templates/documentation.gohtml",
|
|
|
|
|
|
|
|
"templates/admin_dashboard.gohtml",
|
2023-08-05 19:34:09 +00:00
|
|
|
"templates/countries.gohtml",
|
2023-08-05 15:50:40 +00:00
|
|
|
|
2024-02-08 20:30:20 +00:00
|
|
|
"templates/dialogs/copy-year.gohtml",
|
|
|
|
|
2023-08-05 15:50:40 +00:00
|
|
|
"templates/dialogs/add-holiday.gohtml",
|
|
|
|
"templates/dialogs/edit-holiday.gohtml",
|
|
|
|
"templates/dialogs/delete-holiday.gohtml",
|
2023-08-05 19:34:09 +00:00
|
|
|
|
2024-01-01 11:29:59 +00:00
|
|
|
"templates/dialogs/add-country.gohtml",
|
2023-08-05 19:34:09 +00:00
|
|
|
"templates/dialogs/edit-country.gohtml",
|
|
|
|
"templates/dialogs/delete-country.gohtml",
|
2023-08-05 15:50:40 +00:00
|
|
|
)
|
2023-06-23 17:31:02 +00:00
|
|
|
}
|
2023-06-20 14:10:46 +00:00
|
|
|
|
2023-12-31 12:35:29 +00:00
|
|
|
func IncludeHTML(path string) template.HTML {
|
|
|
|
b, err := os.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("includeHTML - error reading file: %v", err)
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return template.HTML(string(b))
|
|
|
|
}
|
|
|
|
|
2023-08-05 19:49:33 +00:00
|
|
|
func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService) {
|
2023-06-23 17:31:02 +00:00
|
|
|
adminDashboard.Use(gin.BasicAuth(loadAuth()))
|
2023-06-20 14:10:46 +00:00
|
|
|
|
2023-08-05 15:50:40 +00:00
|
|
|
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
|
2023-06-20 14:10:46 +00:00
|
|
|
}
|
2023-08-05 15:50:40 +00:00
|
|
|
holidays, _ := service.Find(search, holiday.Paging{PageSize: 100})
|
|
|
|
holidayResponse := mapHolidays(holidays)
|
2023-08-05 19:34:09 +00:00
|
|
|
countries, _ := countryService.Find()
|
2023-08-05 19:49:33 +00:00
|
|
|
years, _ := yearService.Find()
|
2023-08-05 15:50:40 +00:00
|
|
|
|
|
|
|
response := map[string]any{}
|
2023-08-05 19:34:09 +00:00
|
|
|
response["Holidays"] = holidayResponse
|
|
|
|
response["Search"] = search
|
|
|
|
response["Countries"] = countries
|
2023-08-05 19:49:33 +00:00
|
|
|
response["Years"] = years
|
2023-08-05 15:50:40 +00:00
|
|
|
|
|
|
|
c.HTML(http.StatusOK, "admin_dashboard.gohtml", response)
|
2023-06-20 14:10:46 +00:00
|
|
|
})
|
2023-08-05 15:50:40 +00:00
|
|
|
adminDashboard.POST("/holidays", func(c *gin.Context) {
|
2023-06-23 17:31:02 +00:00
|
|
|
request := struct {
|
|
|
|
Id *string `form:"id"`
|
|
|
|
Name string `form:"name" binding:"required,min=1"`
|
|
|
|
Description string `form:"description"`
|
2023-08-05 15:50:40 +00:00
|
|
|
IsStateHoliday bool `form:"state_holiday"`
|
|
|
|
IsReligiousHoliday bool `form:"religious_holiday"`
|
2023-06-23 17:31:02 +00:00
|
|
|
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)
|
2023-08-05 15:50:40 +00:00
|
|
|
return
|
2023-06-20 14:10:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
hol := holiday.Holiday{
|
2023-06-23 17:31:02 +00:00
|
|
|
Country: request.Country,
|
|
|
|
Date: request.Date,
|
|
|
|
Name: request.Name,
|
|
|
|
Description: request.Description,
|
|
|
|
IsStateHoliday: request.IsStateHoliday,
|
|
|
|
IsReligiousHoliday: request.IsReligiousHoliday,
|
2023-06-20 14:10:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
2023-06-23 17:31:02 +00:00
|
|
|
if request.Id != nil {
|
|
|
|
hol.Id = uuid.MustParse(*request.Id)
|
2023-06-20 14:10:46 +00:00
|
|
|
hol, err = service.Update(hol)
|
|
|
|
} else {
|
|
|
|
hol, err = service.Create(hol)
|
|
|
|
}
|
|
|
|
if err != nil {
|
2023-06-23 17:31:02 +00:00
|
|
|
c.AbortWithError(http.StatusInternalServerError, err)
|
2023-06-20 14:10:46 +00:00
|
|
|
} else {
|
2023-08-05 15:50:40 +00:00
|
|
|
c.Redirect(http.StatusSeeOther, "/admin?country="+request.Country+"&year="+strconv.FormatInt(int64(request.Date.Year()), 10))
|
2023-06-20 14:10:46 +00:00
|
|
|
}
|
|
|
|
})
|
2024-02-08 20:30:20 +00:00
|
|
|
adminDashboard.POST("/holidays/copy", func(c *gin.Context) {
|
|
|
|
request := struct {
|
|
|
|
From int `form:"from"`
|
|
|
|
To int `form:"to"`
|
|
|
|
Country string `form:"country" binding:"len=2"`
|
|
|
|
}{}
|
|
|
|
if err := c.ShouldBind(&request); err != nil {
|
|
|
|
c.AbortWithError(http.StatusBadRequest, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err := service.Copy(request.Country, request.From, request.To)
|
|
|
|
if err != nil {
|
|
|
|
c.AbortWithError(http.StatusInternalServerError, err)
|
|
|
|
} else {
|
|
|
|
c.Redirect(http.StatusSeeOther, "/admin?country="+request.Country+"&year="+strconv.FormatInt(int64(request.To), 10))
|
|
|
|
}
|
|
|
|
})
|
2023-08-05 15:50:40 +00:00
|
|
|
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)
|
2023-06-23 17:31:02 +00:00
|
|
|
return
|
2023-06-20 14:10:46 +00:00
|
|
|
}
|
2023-08-05 15:50:40 +00:00
|
|
|
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))
|
|
|
|
})
|
2023-08-05 19:34:09 +00:00
|
|
|
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")
|
|
|
|
})
|
2024-01-01 11:29:59 +00:00
|
|
|
adminDashboard.GET("/dialogs/add-country", func(c *gin.Context) {
|
|
|
|
c.HTML(http.StatusOK, "add-country.gohtml", gin.H{})
|
|
|
|
})
|
2023-08-05 19:34:09 +00:00
|
|
|
|
2023-08-05 15:50:40 +00:00
|
|
|
adminDashboard.GET("/dialogs/add-holiday", func(c *gin.Context) {
|
2023-08-05 19:34:09 +00:00
|
|
|
countries, _ := countryService.Find()
|
|
|
|
c.HTML(http.StatusOK, "add-holiday.gohtml", gin.H{"Countries": countries})
|
2023-08-05 15:50:40 +00:00
|
|
|
})
|
|
|
|
adminDashboard.GET("/dialogs/edit-holiday", func(c *gin.Context) {
|
|
|
|
id := uuid.MustParse(c.Query("id"))
|
|
|
|
hol, err := service.FindById(id)
|
2023-06-23 17:31:02 +00:00
|
|
|
if err != nil {
|
2023-08-05 15:50:40 +00:00
|
|
|
c.AbortWithError(http.StatusNotFound, err)
|
|
|
|
return
|
2023-06-20 14:10:46 +00:00
|
|
|
}
|
2023-08-05 19:34:09 +00:00
|
|
|
countries, _ := countryService.Find()
|
|
|
|
c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Countries": countries, "Holiday": hol})
|
|
|
|
})
|
2024-02-08 20:30:20 +00:00
|
|
|
adminDashboard.GET("/dialogs/copy-year", func(c *gin.Context) {
|
|
|
|
country := c.Query("country")
|
|
|
|
year, err := strconv.ParseInt(c.Query("year"), 10, 32)
|
|
|
|
if err != nil {
|
|
|
|
c.AbortWithError(http.StatusNotFound, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
c.HTML(http.StatusOK, "copy-year.gohtml", gin.H{"Country": country, "Year": year})
|
|
|
|
})
|
2023-08-05 19:34:09 +00:00
|
|
|
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})
|
2023-06-23 17:31:02 +00:00
|
|
|
})
|
2023-08-05 15:50:40 +00:00
|
|
|
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)
|
2023-06-23 17:31:02 +00:00
|
|
|
return
|
|
|
|
}
|
2023-08-05 15:50:40 +00:00
|
|
|
c.HTML(http.StatusOK, "delete-holiday.gohtml", gin.H{"Holiday": hol})
|
2023-06-20 14:10:46 +00:00
|
|
|
})
|
2023-08-05 19:34:09 +00:00
|
|
|
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})
|
|
|
|
})
|
2023-06-20 14:10:46 +00:00
|
|
|
}
|
2023-06-16 07:42:50 +00:00
|
|
|
|
2023-06-23 17:31:02 +00:00
|
|
|
func loadAuth() map[string]string {
|
2023-06-20 14:10:46 +00:00
|
|
|
credentials := envMustExist("AUTH_KEY")
|
|
|
|
values := strings.Split(credentials, ":")
|
|
|
|
return map[string]string{values[0]: values[1]}
|
2023-06-16 07:42:50 +00:00
|
|
|
}
|