Code reorganization + added meta tags and favicon
This commit is contained in:
Borna Rajković committed 2024-02-10 17:49:00 +01:00
1 parent 45b220c69f
commit c6f365ab28
22 files changed
+566 -415

No files matched your search

+351
View File
@@ -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 "-"
}
+39
View File
@@ -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
}
}
+39
View File
@@ -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())
}
+22
View File
@@ -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
}
+43
View File
@@ -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",
)
}