From c6f365ab28fcce5566514f230566e4a8625a861f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20Rajkovi=C4=87?= Date: Sat, 10 Feb 2024 17:47:14 +0100 Subject: [PATCH] v1.0.0 Code reorganization + added meta tags and favicon --- .gitignore | 1 + api/api.go | 351 ++++++++++++++++++ api/middleware.go | 39 ++ api/routes.go | 39 ++ api/server.go | 22 ++ api/templates.go | 43 +++ assets/favicon.ico | Bin 0 -> 766 bytes db.go | 16 +- db/database.go | 30 ++ .../holiday}/country_service.go | 0 handlers.go => domain/holiday/dto.go | 50 +-- .../holiday}/holiday_service.go | 0 {holiday => domain/holiday}/model.go | 0 {holiday => domain/holiday}/year_service.go | 0 main.go | 318 +--------------- makefile | 2 +- render.go | 42 --- templates/admin_dashboard.gohtml | 2 +- templates/countries.gohtml | 2 +- templates/documentation.gohtml | 8 +- templates/index.gohtml | 8 +- templates/results.gohtml | 8 +- 22 files changed, 566 insertions(+), 415 deletions(-) create mode 100644 api/api.go create mode 100644 api/middleware.go create mode 100644 api/routes.go create mode 100644 api/server.go create mode 100644 api/templates.go create mode 100644 assets/favicon.ico create mode 100644 db/database.go rename {holiday => domain/holiday}/country_service.go (100%) rename handlers.go => domain/holiday/dto.go (62%) rename {holiday => domain/holiday}/holiday_service.go (100%) rename {holiday => domain/holiday}/model.go (100%) rename {holiday => domain/holiday}/year_service.go (100%) delete mode 100644 render.go diff --git a/.gitignore b/.gitignore index 4cbb524..b4fad44 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ holiday-api .env **/.DS_Store +.env* \ No newline at end of file diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..60d0d91 --- /dev/null +++ b/api/api.go @@ -0,0 +1,351 @@ +package api + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "holiday-api/domain/holiday" + "log" + "net/http" + "strconv" + "time" +) + +func NoMethod() gin.HandlerFunc { + return func(c *gin.Context) { + c.AbortWithError(http.StatusNotFound, nil) + } +} + +func NoRoute() gin.HandlerFunc { + return func(c *gin.Context) { + c.AbortWithError(http.StatusNotFound, nil) + } +} + +func GetIndex(holidayService holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService) gin.HandlerFunc { + return func(c *gin.Context) { + search := holiday.Search{Country: "HR", Year: nil} + if err := c.ShouldBindQuery(&search); err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + years, _ := yearService.Find() + if search.Year == nil { + c.HTML(http.StatusOK, "index.gohtml", gin.H{"Year": time.Now().Year(), "Years": years}) + return + } + countries, _ := countryService.Find() + holidays, _ := holidayService.Find(search, holiday.Paging{PageSize: 100}) + c.HTML(http.StatusOK, "results.gohtml", gin.H{"Years": years, "Countries": countries, "Search": search, "Holidays": holiday.ToResponse(holidays).Holidays}) + } +} + +func GetDocumentation(countryService holiday.CountryService, yearService holiday.YearService) gin.HandlerFunc { + return func(c *gin.Context) { + countries, _ := countryService.Find() + years, _ := yearService.Find() + c.HTML(http.StatusOK, "documentation.gohtml", gin.H{"Year": time.Now().Year(), "Years": years, "Countries": countries}) + } +} + +func GetHolidays(service holiday.HolidayService) gin.HandlerFunc { + return func(c *gin.Context) { + paging := holiday.Paging{PageSize: 50} + if err := c.ShouldBindQuery(&paging); err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + search := holiday.Search{} + if err := c.ShouldBindQuery(&search); err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + holidays, err := service.Find(search, paging) + if err != nil { + render(c, http.StatusNotFound, holiday.ErrorResponse{Created: time.Now(), Message: "failed fetching holidays"}, nil) + } else { + render(c, http.StatusOK, holiday.ToResponse(holidays), search.Type) + } + } +} + +func DeleteCountry(countryService holiday.CountryService) gin.HandlerFunc { + return func(c *gin.Context) { + id := uuid.MustParse(c.Param("id")) + _, err := countryService.FindById(id) + if err != nil { + Abort(c, err, http.StatusNotFound, "couldn't find country") + return + } + if err := countryService.Delete(id); err != nil { + Abort(c, err, http.StatusInternalServerError, "couldn't delete country") + return + } + c.Redirect(http.StatusSeeOther, "/admin/countries") + } +} + +func CreateOrUpdateCountry(countryService holiday.CountryService) gin.HandlerFunc { + return 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 { + Abort(c, err, http.StatusInternalServerError, "invalid request when creating or updating country") + 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 { + Abort(c, err, http.StatusInternalServerError, "couldn't create or update country") + } else { + c.Redirect(http.StatusSeeOther, "/admin/countries") + } + } +} + +func GetCountries(countryService holiday.CountryService) gin.HandlerFunc { + return func(c *gin.Context) { + countries, _ := countryService.Find() + c.HTML(http.StatusOK, "countries.gohtml", gin.H{"Countries": countries}) + } +} + +func DeleteHoliday(holidayService holiday.HolidayService) gin.HandlerFunc { + return func(c *gin.Context) { + id := uuid.MustParse(c.Param("id")) + hol, err := holidayService.FindById(id) + if err != nil { + Abort(c, err, http.StatusNotFound, "couldn't find holiday to delete") + return + } + if err := holidayService.Delete(id); err != nil { + Abort(c, err, http.StatusInternalServerError, "couldn't delete holiday") + return + } + c.Redirect(http.StatusSeeOther, "/admin?country="+hol.Country+"&year="+strconv.FormatInt(int64(hol.Date.Year()), 10)) + } +} + +func CopyYear(holidayService holiday.HolidayService) gin.HandlerFunc { + return 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 { + Abort(c, err, http.StatusBadRequest, "invalid request when copying holiday") + return + } + + err := holidayService.Copy(request.Country, request.From, request.To) + if err != nil { + Abort(c, err, http.StatusInternalServerError, "couldn't copy holidays") + } else { + c.Redirect(http.StatusSeeOther, "/admin?country="+request.Country+"&year="+strconv.FormatInt(int64(request.To), 10)) + } + } +} + +func CreateOrUpdateHoliday(holidayService holiday.HolidayService) gin.HandlerFunc { + return 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 { + Abort(c, err, http.StatusBadRequest, "invalid request when creating holiday") + 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 = holidayService.Update(hol) + } else { + hol, err = holidayService.Create(hol) + } + if err != nil { + Abort(c, err, 500, "couldn't create or update holiday") + } else { + c.Redirect(http.StatusSeeOther, "/admin?country="+request.Country+"&year="+strconv.FormatInt(int64(request.Date.Year()), 10)) + } + } +} + +func GetAdminHome(holidayService holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService) gin.HandlerFunc { + return func(c *gin.Context) { + search := holiday.Search{Country: "HR", Year: new(int)} + *search.Year = time.Now().Year() + if err := c.ShouldBindQuery(&search); err != nil { + Abort(c, err, http.StatusBadRequest, "invalid search parameters") + return + } + holidays, _ := holidayService.Find(search, holiday.Paging{PageSize: 100}) + holidayResponse := holiday.ToResponse(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) + } +} + +func AddHolidayDialog(countryService holiday.CountryService) gin.HandlerFunc { + return func(c *gin.Context) { + countries, _ := countryService.Find() + c.HTML(http.StatusOK, "add-holiday.gohtml", gin.H{"Countries": countries}) + } +} + +func EditHolidayDialog(holidayService holiday.HolidayService, countryService holiday.CountryService) gin.HandlerFunc { + return func(c *gin.Context) { + id := uuid.MustParse(c.Query("id")) + hol, err := holidayService.FindById(id) + if err != nil { + Abort(c, err, http.StatusNotFound, "couldn't find holiday to edit") + return + } + countries, _ := countryService.Find() + c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Countries": countries, "Holiday": hol}) + } +} + +func DeleteHolidayDialog(holidayService holiday.HolidayService) gin.HandlerFunc { + return func(c *gin.Context) { + id := uuid.MustParse(c.Query("id")) + hol, err := holidayService.FindById(id) + if err != nil { + Abort(c, err, http.StatusNotFound, "couldn't find dialog") + return + } + c.HTML(http.StatusOK, "delete-holiday.gohtml", gin.H{"Holiday": hol}) + } +} + +func CopyYearDialog() gin.HandlerFunc { + return func(c *gin.Context) { + country := c.Query("country") + year, err := strconv.ParseInt(c.Query("year"), 10, 32) + if err != nil { + Abort(c, err, http.StatusNotFound, "couldn't parse year") + return + } + c.HTML(http.StatusOK, "copy-year.gohtml", gin.H{"Country": country, "Year": year}) + } +} + +func AddCountryDialog() gin.HandlerFunc { + return func(c *gin.Context) { + c.HTML(http.StatusOK, "add-country.gohtml", gin.H{}) + } +} + +func EditCountryDialog(countryService holiday.CountryService) gin.HandlerFunc { + return func(c *gin.Context) { + id := uuid.MustParse(c.Query("id")) + country, err := countryService.FindById(id) + if err != nil { + Abort(c, err, http.StatusNotFound, "couldn't find country") + return + } + c.HTML(http.StatusOK, "edit-country.gohtml", gin.H{"Country": country}) + } +} + +func DeleteCountryDialog(countryService holiday.CountryService) gin.HandlerFunc { + return func(c *gin.Context) { + id := uuid.MustParse(c.Query("id")) + country, err := countryService.FindById(id) + if err != nil { + Abort(c, err, http.StatusNotFound, "couldn't find dialog") + return + } + c.HTML(http.StatusOK, "delete-country.gohtml", gin.H{"Country": country}) + } +} + +type CSV interface { + CSV() []byte +} + +func render(c *gin.Context, status int, response any, contentType *string) { + value := c.GetHeader("accept") + if contentType != nil { + switch *contentType { + case "xml": + value = "text/xml" + case "json": + value = "application/json" + case "csv": + value = "text/csv" + } + } + switch value { + case "text/csv": + if csvResponse, ok := response.(CSV); ok { + c.Data(200, value+"; charset=utf-8", csvResponse.CSV()) + } else { + c.Header("content-type", "application/json; charset=utf-8") + c.JSON(status, response) + } + case "application/xml": + fallthrough + case "text/xml": + c.Header("content-type", value+"; charset=utf-8; header=present;") + c.XML(status, response) + case "application/json": + fallthrough + default: + c.Header("content-type", "application/json; charset=utf-8") + c.JSON(status, response) + } +} + +func Abort(c *gin.Context, err error, statusCode int, message string) { + log.Output(1, fmt.Sprintf("error | %s | err: %v", message, errorMessage(err))) + c.AbortWithError(statusCode, err) +} + +func errorMessage(err error) string { + if err != nil { + return err.Error() + } + return "-" +} diff --git a/api/middleware.go b/api/middleware.go new file mode 100644 index 0000000..98885f8 --- /dev/null +++ b/api/middleware.go @@ -0,0 +1,39 @@ +package api + +import ( + "fmt" + "github.com/gin-gonic/gin" + "os" + "strings" +) + +func Auth() gin.HandlerFunc { + if hasProfile("basic-auth") { + return gin.BasicAuth(loadAuth()) + } + panic("keycloak support not implemented") +} + +func hasProfile(value string) bool { + profileOptions := strings.Split(os.Getenv("PROFILE"), ",") + for _, option := range profileOptions { + if option == value { + return true + } + } + return false +} + +func loadAuth() map[string]string { + credentials := envMustExist("AUTH_KEY") + values := strings.Split(credentials, ":") + return map[string]string{values[0]: values[1]} +} + +func envMustExist(env string) string { + if value, exists := os.LookupEnv(env); !exists { + panic(fmt.Sprintf("env variable '%s' not defined", env)) + } else { + return value + } +} diff --git a/api/routes.go b/api/routes.go new file mode 100644 index 0000000..d9a6a17 --- /dev/null +++ b/api/routes.go @@ -0,0 +1,39 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/jmoiron/sqlx" + "holiday-api/domain/holiday" +) + +func RegisterRoutes(server *gin.Engine, db *sqlx.DB) { + + holidayService := holiday.HolidayService{DB: db} + countryService := holiday.CountryService{DB: db} + yearService := holiday.YearService{DB: db} + + server.GET("/", GetIndex(holidayService, countryService, yearService)) + server.GET("/documentation", GetDocumentation(countryService, yearService)) + + server.GET("/api/v1/holidays", GetHolidays(holidayService)) + + auth := Auth() + + adminGroup := server.Group("/admin", auth) + adminGroup.GET("/", GetAdminHome(holidayService, countryService, yearService)) + adminGroup.POST("/holidays", CreateOrUpdateHoliday(holidayService)) + adminGroup.POST("/holidays/copy", CopyYear(holidayService)) + adminGroup.POST("/holidays/:id/delete", DeleteHoliday(holidayService)) + adminGroup.GET("countries", GetCountries(countryService)) + adminGroup.POST("countries", CreateOrUpdateCountry(countryService)) + adminGroup.POST("countries/:id/delete", DeleteCountry(countryService)) + + dialogGroup := server.Group("/admin/dialogs", auth) + dialogGroup.GET("/add-country", AddCountryDialog()) + dialogGroup.GET("/edit-country", EditCountryDialog(countryService)) + dialogGroup.GET("/delete-country", DeleteCountryDialog(countryService)) + dialogGroup.GET("/add-holiday", AddHolidayDialog(countryService)) + dialogGroup.GET("/edit-holiday", EditHolidayDialog(holidayService, countryService)) + dialogGroup.GET("/delete-holiday", DeleteHolidayDialog(holidayService)) + dialogGroup.GET("/copy-year", CopyYearDialog()) +} diff --git a/api/server.go b/api/server.go new file mode 100644 index 0000000..fdd32c9 --- /dev/null +++ b/api/server.go @@ -0,0 +1,22 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/jmoiron/sqlx" +) + +func SetupServer(db *sqlx.DB) *gin.Engine { + server := createServer() + LoadTemplates(server) + RegisterRoutes(server, db) + return server +} + +func createServer() *gin.Engine { + server := gin.New() + server.NoRoute(NoRoute()) + server.NoMethod(NoMethod()) + server.Use(gin.Recovery()) + server.Static("assets", "assets") + return server +} diff --git a/api/templates.go b/api/templates.go new file mode 100644 index 0000000..56d5f61 --- /dev/null +++ b/api/templates.go @@ -0,0 +1,43 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "html/template" +) + +func LoadTemplates(server *gin.Engine) { + server.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 + }, + }) + server.LoadHTMLFiles( + "templates/index.gohtml", + "templates/results.gohtml", + "templates/documentation.gohtml", + + "templates/admin_dashboard.gohtml", + "templates/countries.gohtml", + + "templates/dialogs/copy-year.gohtml", + + "templates/dialogs/add-holiday.gohtml", + "templates/dialogs/edit-holiday.gohtml", + "templates/dialogs/delete-holiday.gohtml", + + "templates/dialogs/add-country.gohtml", + "templates/dialogs/edit-country.gohtml", + "templates/dialogs/delete-country.gohtml", + ) +} diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..857530b161a1c35a1ebaf692fe4c7d688ea89501 GIT binary patch literal 766 zcmZQzU<5)11py$*!tjELfkBLcfk6X^6@b_Qh(Y4GfU&VLLqw=9LqU!zLvx)BLupAe z!{lB*hFLQk7`APl&T#h3DqLF06jwq3MW7L)p-8}hfx#4rVSqW1fgu#e0ntG~K4WMw zb7-JwsDWvy0Eo{N8p;|Pq#7C!8md4HpFv5HQAvSGNkB*mY#xx$l9qtr1I^>;XhSg% zFCXY0_Kp^Wd(ho$gz&F2$bHBFE8l<-tRxs1G$8q4W}s(*z`#@x#8(A+l@-KS0{S?B gfq@B$A1nZ}Ton>zOdwfod{C%@fGJotggsn+a literal 0 HcmV?d00001 diff --git a/db.go b/db.go index 85fa139..6c7fae9 100644 --- a/db.go +++ b/db.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" + "holiday-api/db" "os" ) @@ -22,18 +23,5 @@ func connectToDb() (*sqlx.DB, error) { password := envMustExist("PSQL_PASSWORD") dbname := envMustExist("PSQL_DB") - psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", - host, port, user, password, dbname) - - db, err := sqlx.Open("postgres", psqlInfo) - if err != nil { - return nil, err - } - - err = db.Ping() - if err != nil { - return nil, err - } - - return db, nil + return db.ConnectToDbNamed(host, port, user, password, dbname) } diff --git a/db/database.go b/db/database.go new file mode 100644 index 0000000..6a57dc0 --- /dev/null +++ b/db/database.go @@ -0,0 +1,30 @@ +package db + +import ( + "embed" + "fmt" + "github.com/jmoiron/sqlx" +) + +//go:embed dev/*.sql +var DevMigrations embed.FS + +//go:embed prod/*.sql +var ProdMigrations embed.FS + +func ConnectToDbNamed(host string, port string, user string, password string, dbname string) (*sqlx.DB, error) { + psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname) + + db, err := sqlx.Open("postgres", psqlInfo) + if err != nil { + return nil, err + } + + err = db.Ping() + if err != nil { + return nil, err + } + + return db, nil +} diff --git a/holiday/country_service.go b/domain/holiday/country_service.go similarity index 100% rename from holiday/country_service.go rename to domain/holiday/country_service.go diff --git a/handlers.go b/domain/holiday/dto.go similarity index 62% rename from handlers.go rename to domain/holiday/dto.go index 5e3af2f..4f09b34 100644 --- a/handlers.go +++ b/domain/holiday/dto.go @@ -1,41 +1,15 @@ -package main +package holiday import ( "bytes" "encoding/csv" "encoding/xml" "fmt" - "github.com/gin-gonic/gin" "github.com/google/uuid" - "holiday-api/holiday" - "net/http" "strconv" "time" ) -func getHolidays(service holiday.HolidayService) gin.HandlerFunc { - return func(c *gin.Context) { - paging := holiday.Paging{PageSize: 50} - if err := c.ShouldBindQuery(&paging); err != nil { - c.AbortWithError(http.StatusBadRequest, err) - return - } - - search := holiday.Search{} - if err := c.ShouldBindQuery(&search); err != nil { - c.AbortWithError(http.StatusBadRequest, err) - return - } - - holidays, err := service.Find(search, paging) - if err != nil { - render(c, http.StatusNotFound, ErrorResponse{Created: time.Now(), Message: "failed fetching holidays"}, nil) - } else { - render(c, http.StatusOK, mapHolidays(holidays), search.Type) - } - } -} - type DateResponse struct{ time.Time } func (d DateResponse) MarshalJSON() ([]byte, error) { @@ -79,27 +53,7 @@ type HolidayItemResponse struct { IsReligiousHoliday bool `json:"isReligiousHoliday" xml:"isReligious,attr"` } -type HolidaySingleResponse struct { - Id *uuid.UUID - Country string - Date DateResponse - Name string - Description string - IsStateHoliday bool - IsReligiousHoliday bool -} - -type HolidaySingleRequest struct { - Id *uuid.UUID - Country string - Date DateResponse - Name string - Description string - IsStateHoliday bool - IsReligiousHoliday bool -} - -func mapHolidays(holidays []holiday.Holiday) HolidayResponse { +func ToResponse(holidays []Holiday) HolidayResponse { var response = make([]HolidayItemResponse, 0, len(holidays)) for _, h := range holidays { response = append(response, HolidayItemResponse{ diff --git a/holiday/holiday_service.go b/domain/holiday/holiday_service.go similarity index 100% rename from holiday/holiday_service.go rename to domain/holiday/holiday_service.go diff --git a/holiday/model.go b/domain/holiday/model.go similarity index 100% rename from holiday/model.go rename to domain/holiday/model.go diff --git a/holiday/year_service.go b/domain/holiday/year_service.go similarity index 100% rename from holiday/year_service.go rename to domain/holiday/year_service.go diff --git a/main.go b/main.go index 907af6a..49012ba 100644 --- a/main.go +++ b/main.go @@ -1,331 +1,51 @@ 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/api" + "holiday-api/db" "holiday-api/migration" - "html/template" + "io/fs" "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() + db, 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 { + if err := migration.InitializeMigrations(db, migrationFolder()); err != nil { log.Fatalf("couldn't execute migrations: %v", err) } - g := gin.Default() + server := api.SetupServer(db) - g.Static("assets", "assets") - - loadTemplates(g) - - holidayService := holiday.HolidayService{DB: client} - countryService := holiday.CountryService{DB: client} - yearService := holiday.YearService{DB: client} - - g.GET("/api/v1/holidays", getHolidays(holidayService)) - - setupAdminDashboard(g.Group("/admin"), holidayService, countryService, yearService) - - g.GET("/", func(c *gin.Context) { - search := holiday.Search{Country: "HR", Year: nil} - if err := c.ShouldBindQuery(&search); err != nil { - c.AbortWithError(http.StatusBadRequest, err) - return - } - years, _ := yearService.Find() - if search.Year == nil { - c.HTML(http.StatusOK, "index.gohtml", gin.H{"Year": time.Now().Year(), "Years": years}) - return - } - countries, _ := countryService.Find() - 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}) - }) - g.GET("/documentation", func(c *gin.Context) { - countries, _ := countryService.Find() - years, _ := yearService.Find() - c.HTML(http.StatusOK, "documentation.gohtml", gin.H{"Year": time.Now().Year(), "Years": years, "Countries": countries}) - }) - log.Fatal(http.ListenAndServe(":5281", g)) + log.Fatal(http.ListenAndServe(":5281", server)) } -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 - }, - "importSvg": IncludeHTML, - }) - g.LoadHTMLFiles( - "templates/index.gohtml", - "templates/results.gohtml", - "templates/documentation.gohtml", - - "templates/admin_dashboard.gohtml", - "templates/countries.gohtml", - - "templates/dialogs/copy-year.gohtml", - - "templates/dialogs/add-holiday.gohtml", - "templates/dialogs/edit-holiday.gohtml", - "templates/dialogs/delete-holiday.gohtml", - - "templates/dialogs/add-country.gohtml", - "templates/dialogs/edit-country.gohtml", - "templates/dialogs/delete-country.gohtml", - ) -} - -func IncludeHTML(path string) template.HTML { - b, err := os.ReadFile(path) - if err != nil { - log.Println("includeHTML - error reading file: %v", err) - return "" +func hasProfile(value string) bool { + profileOptions := strings.Split(os.Getenv("PROFILE"), ",") + for _, option := range profileOptions { + if option == value { + return true + } } - return template.HTML(string(b)) + return false } -func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService) { - 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/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)) - } - }) - 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("/dialogs/add-country", func(c *gin.Context) { - c.HTML(http.StatusOK, "add-country.gohtml", gin.H{}) - }) - - 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/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/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}) - }) - 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}) - }) -} - -func loadAuth() map[string]string { - credentials := envMustExist("AUTH_KEY") - values := strings.Split(credentials, ":") - return map[string]string{values[0]: values[1]} +func migrationFolder() fs.FS { + if hasProfile("dev") { + return db.DevMigrations + } + return db.ProdMigrations } diff --git a/makefile b/makefile index 6fb7f0f..f90dbdb 100644 --- a/makefile +++ b/makefile @@ -1,7 +1,7 @@ # scripts for building app # requires go 1.19+ and git installed -VERSION := 0.5.0 +VERSION := 1.0.0 serve: go run ./... diff --git a/render.go b/render.go deleted file mode 100644 index b28a16f..0000000 --- a/render.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "github.com/gin-gonic/gin" -) - -type CSV interface { - CSV() []byte -} - -func render(c *gin.Context, status int, response any, contentType *string) { - value := c.GetHeader("accept") - if contentType != nil { - switch *contentType { - case "xml": - value = "text/xml" - case "json": - value = "application/json" - case "csv": - value = "text/csv" - } - } - switch value { - case "text/csv": - if csvResponse, ok := response.(CSV); ok { - c.Data(200, value+"; charset=utf-8", csvResponse.CSV()) - } else { - c.Header("content-type", "application/json; charset=utf-8") - c.JSON(status, response) - } - case "application/xml": - fallthrough - case "text/xml": - c.Header("content-type", value+"; charset=utf-8; header=present;") - c.XML(status, response) - case "application/json": - fallthrough - default: - c.Header("content-type", "application/json; charset=utf-8") - c.JSON(status, response) - } -} diff --git a/templates/admin_dashboard.gohtml b/templates/admin_dashboard.gohtml index 1ed3ae5..537b837 100644 --- a/templates/admin_dashboard.gohtml +++ b/templates/admin_dashboard.gohtml @@ -5,7 +5,7 @@ - + diff --git a/templates/countries.gohtml b/templates/countries.gohtml index 1f91072..1ebfe9d 100644 --- a/templates/countries.gohtml +++ b/templates/countries.gohtml @@ -5,7 +5,7 @@ - + diff --git a/templates/documentation.gohtml b/templates/documentation.gohtml index 20b0b22..1a82cf6 100644 --- a/templates/documentation.gohtml +++ b/templates/documentation.gohtml @@ -1,11 +1,13 @@ - + - Holiday-api | Dokumentacija + Holiday-api | Api dokumentacija - + + + diff --git a/templates/index.gohtml b/templates/index.gohtml index 5145bc9..f1ee0f8 100644 --- a/templates/index.gohtml +++ b/templates/index.gohtml @@ -1,11 +1,13 @@ - + - Holiday-api + Holiday-api | Pronađi praznike - + + + diff --git a/templates/results.gohtml b/templates/results.gohtml index 50ae0b1..21b345a 100644 --- a/templates/results.gohtml +++ b/templates/results.gohtml @@ -1,11 +1,13 @@ - + - Holiday-api | {{deferint $.Search.Year}} + Holiday-api | Praznici za {{deferint $.Search.Year}} - + + + -- 2.40.1