parent
45b220c69f
commit
c6f365ab28
|
@ -2,3 +2,4 @@
|
||||||
holiday-api
|
holiday-api
|
||||||
.env
|
.env
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
.env*
|
|
@ -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 "-"
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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",
|
||||||
|
)
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 766 B |
16
db.go
16
db.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
|
"holiday-api/db"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,18 +23,5 @@ func connectToDb() (*sqlx.DB, error) {
|
||||||
password := envMustExist("PSQL_PASSWORD")
|
password := envMustExist("PSQL_PASSWORD")
|
||||||
dbname := envMustExist("PSQL_DB")
|
dbname := envMustExist("PSQL_DB")
|
||||||
|
|
||||||
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
return db.ConnectToDbNamed(host, port, user, password, dbname)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,41 +1,15 @@
|
||||||
package main
|
package holiday
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"holiday-api/holiday"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"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 }
|
type DateResponse struct{ time.Time }
|
||||||
|
|
||||||
func (d DateResponse) MarshalJSON() ([]byte, error) {
|
func (d DateResponse) MarshalJSON() ([]byte, error) {
|
||||||
|
@ -79,27 +53,7 @@ type HolidayItemResponse struct {
|
||||||
IsReligiousHoliday bool `json:"isReligiousHoliday" xml:"isReligious,attr"`
|
IsReligiousHoliday bool `json:"isReligiousHoliday" xml:"isReligious,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HolidaySingleResponse struct {
|
func ToResponse(holidays []Holiday) HolidayResponse {
|
||||||
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 {
|
|
||||||
var response = make([]HolidayItemResponse, 0, len(holidays))
|
var response = make([]HolidayItemResponse, 0, len(holidays))
|
||||||
for _, h := range holidays {
|
for _, h := range holidays {
|
||||||
response = append(response, HolidayItemResponse{
|
response = append(response, HolidayItemResponse{
|
318
main.go
318
main.go
|
@ -1,331 +1,51 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
"holiday-api/holiday"
|
"holiday-api/api"
|
||||||
|
"holiday-api/db"
|
||||||
"holiday-api/migration"
|
"holiday-api/migration"
|
||||||
"html/template"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"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() {
|
func init() {
|
||||||
godotenv.Load()
|
godotenv.Load()
|
||||||
if strings.Contains(os.Getenv("PROFILE"), "dev") {
|
|
||||||
isDev = true
|
|
||||||
}
|
|
||||||
log.SetPrefix("")
|
log.SetPrefix("")
|
||||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
client, err := connectToDb()
|
db, err := connectToDb()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("couldn't connect to db: %v", err)
|
log.Fatalf("couldn't connect to db: %v", err)
|
||||||
}
|
}
|
||||||
migrationFolder := prodMigrations
|
if err := migration.InitializeMigrations(db, migrationFolder()); err != nil {
|
||||||
if isDev {
|
|
||||||
migrationFolder = devMigrations
|
|
||||||
}
|
|
||||||
if err := migration.InitializeMigrations(client, migrationFolder); err != nil {
|
|
||||||
log.Fatalf("couldn't execute migrations: %v", err)
|
log.Fatalf("couldn't execute migrations: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
g := gin.Default()
|
server := api.SetupServer(db)
|
||||||
|
|
||||||
g.Static("assets", "assets")
|
log.Fatal(http.ListenAndServe(":5281", server))
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTemplates(g *gin.Engine) {
|
func hasProfile(value string) bool {
|
||||||
g.SetFuncMap(template.FuncMap{
|
profileOptions := strings.Split(os.Getenv("PROFILE"), ",")
|
||||||
"boolcmp": func(value *bool, expected string) bool {
|
for _, option := range profileOptions {
|
||||||
if value == nil {
|
if option == value {
|
||||||
return expected == "nil"
|
return true
|
||||||
} 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 ""
|
|
||||||
}
|
}
|
||||||
return template.HTML(string(b))
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService) {
|
func migrationFolder() fs.FS {
|
||||||
adminDashboard.Use(gin.BasicAuth(loadAuth()))
|
if hasProfile("dev") {
|
||||||
|
return db.DevMigrations
|
||||||
adminDashboard.GET("/", func(c *gin.Context) {
|
}
|
||||||
search := holiday.Search{Country: "HR", Year: new(int)}
|
return db.ProdMigrations
|
||||||
*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]}
|
|
||||||
}
|
}
|
||||||
|
|
2
makefile
2
makefile
|
@ -1,7 +1,7 @@
|
||||||
# scripts for building app
|
# scripts for building app
|
||||||
# requires go 1.19+ and git installed
|
# requires go 1.19+ and git installed
|
||||||
|
|
||||||
VERSION := 0.5.0
|
VERSION := 1.0.0
|
||||||
|
|
||||||
serve:
|
serve:
|
||||||
go run ./...
|
go run ./...
|
||||||
|
|
42
render.go
42
render.go
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,7 +5,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="/assets/global.css">
|
<link rel="stylesheet" href="/assets/global.css">
|
||||||
<script src="/assets/global.js"></script>
|
<script src="/assets/global.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="/assets/global.css">
|
<link rel="stylesheet" href="/assets/global.css">
|
||||||
<script src="/assets/global.js"></script>
|
<script src="/assets/global.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="hr">
|
||||||
<head>
|
<head>
|
||||||
<title>Holiday-api | Dokumentacija</title>
|
<title>Holiday-api | Api dokumentacija</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
<meta name="description" content="Preuzmi praznike za neki vremenski raspon programski u JSON, XML ili CSV formatu"/>
|
||||||
|
|
||||||
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="/assets/global.css">
|
<link rel="stylesheet" href="/assets/global.css">
|
||||||
<script src="/assets/global.js"></script>
|
<script src="/assets/global.js"></script>
|
||||||
<script src="/assets/documentation.js"></script>
|
<script src="/assets/documentation.js"></script>
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="hr">
|
||||||
<head>
|
<head>
|
||||||
<title>Holiday-api</title>
|
<title>Holiday-api | Pronađi praznike</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
<meta name="description" content="Pronađi listu praznike za neki vremenski raspon, ili kao web stranicu ili programski koristeći naš api u JSON, XML ili CSV formatu"/>
|
||||||
|
|
||||||
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="/assets/global.css">
|
<link rel="stylesheet" href="/assets/global.css">
|
||||||
<script src="/assets/global.js"></script>
|
<script src="/assets/global.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="hr">
|
||||||
<head>
|
<head>
|
||||||
<title>Holiday-api | {{deferint $.Search.Year}}</title>
|
<title>Holiday-api | Praznici za {{deferint $.Search.Year}}</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
<meta name="description" content="Lista praznika za godinu {{deferint $.Search.Year}}. Praznici ovo ili neku drugu godinu pronađi ovdje. "/>
|
||||||
|
|
||||||
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="/assets/global.css">
|
<link rel="stylesheet" href="/assets/global.css">
|
||||||
<script src="/assets/global.js"></script>
|
<script src="/assets/global.js"></script>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue