diff --git a/go.mod b/go.mod index eaf5698..7bd68bc 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,12 @@ go 1.21.6 require ( github.com/go-playground/validator/v10 v10.18.0 - github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt/v5 v5.2.0 - github.com/gorilla/mux v1.8.1 github.com/gorilla/sessions v1.2.2 github.com/gosimple/slug v1.13.1 + github.com/labstack/echo/v4 v4.11.4 + github.com/lib/pq v1.10.9 + github.com/pkg/errors v0.9.1 github.com/spf13/viper v1.18.2 github.com/temporalio/ui-server/v2 v2.23.0 go.temporal.io/sdk v1.26.0-rc.2 @@ -53,6 +54,7 @@ require ( github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/gocql/gocql v1.5.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/mock v1.7.0-rc.1 // indirect @@ -78,10 +80,8 @@ require ( github.com/jmoiron/sqlx v1.3.4 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/labstack/echo/v4 v4.9.1 // indirect - github.com/labstack/gommon v0.4.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.10.9 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -94,7 +94,6 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect @@ -121,7 +120,7 @@ require ( github.com/uber-common/bark v1.3.0 // indirect github.com/uber-go/tally/v4 v4.1.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.1 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect diff --git a/go.sum b/go.sum index 3a6ce44..814ff6d 100644 --- a/go.sum +++ b/go.sum @@ -191,8 +191,6 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= @@ -254,10 +252,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y= -github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= +github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -267,10 +265,8 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -412,8 +408,8 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -564,11 +560,8 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/handlers/404.go b/internal/handlers/404.go index 32ecfed..c558797 100644 --- a/internal/handlers/404.go +++ b/internal/handlers/404.go @@ -1,33 +1,15 @@ package handlers import ( - "fmt" "net/http" - "text/template" - "code.tjo.space/mentos1386/zdravko/web/templates" "code.tjo.space/mentos1386/zdravko/web/templates/components" + "github.com/labstack/echo/v4" ) -func (h *BaseHandler) Error404(w http.ResponseWriter, r *http.Request) { - ts, err := template.ParseFS(templates.Templates, - "components/base.tmpl", - "pages/404.tmpl", - ) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNotFound) - - err = ts.ExecuteTemplate(w, "base", &components.Base{ +func (h *BaseHandler) Error404(c echo.Context) error { + return c.Render(http.StatusNotFound, "404.tmpl", &components.Base{ NavbarActive: nil, Navbar: Pages, }) - if err != nil { - fmt.Println("Error", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } } diff --git a/internal/handlers/api.go b/internal/handlers/api.go index c6cf332..607c81f 100644 --- a/internal/handlers/api.go +++ b/internal/handlers/api.go @@ -1,8 +1,12 @@ package handlers import ( - "encoding/json" + "context" "net/http" + + "code.tjo.space/mentos1386/zdravko/internal/models" + "code.tjo.space/mentos1386/zdravko/internal/services" + "github.com/labstack/echo/v4" ) type ApiV1WorkersConnectGETResponse struct { @@ -11,25 +15,38 @@ type ApiV1WorkersConnectGETResponse struct { Slug string `json:"slug"` } -func (h *BaseHandler) ApiV1WorkersConnectGET(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { - // Json response containing temporal endpoint - w.Header().Set("Content-Type", "application/json") +func (h *BaseHandler) ApiV1WorkersConnectGET(c echo.Context) error { + cc := c.(AuthenticatedContext) response := ApiV1WorkersConnectGETResponse{ Endpoint: h.config.Temporal.ServerHost, - Group: principal.Worker.Group, - Slug: principal.Worker.Slug, + Group: cc.Principal.Worker.Group, + Slug: cc.Principal.Worker.Slug, } - responseJson, err := json.Marshal(response) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - _, err = w.Write(responseJson) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + return c.JSON(http.StatusOK, response) +} + +// TODO: Can we instead get this from the Workflow outcome? +// +// To somehow listen for the outcomes and then store them automatically. +func (h *BaseHandler) ApiV1HealthchecksHistoryPOST(c echo.Context) error { + ctx := context.Background() + + slug := c.Param("slug") + + healthcheck, err := services.GetHealthcheckHttp(ctx, h.query, slug) + if err != nil { + return err + } + + err = h.query.HealthcheckHttp.History.Model(healthcheck).Append( + &models.HealthcheckHttpHistory{ + Status: "UP", + }) + if err != nil { + return err + } + + return c.JSON(http.StatusCreated, map[string]string{"status": "ok"}) } diff --git a/internal/handlers/authentication.go b/internal/handlers/authentication.go index e85544b..52a6eeb 100644 --- a/internal/handlers/authentication.go +++ b/internal/handlers/authentication.go @@ -3,12 +3,12 @@ package handlers import ( "context" "fmt" - "log" "net/http" "strings" "time" jwtInternal "code.tjo.space/mentos1386/zdravko/internal/jwt" + "github.com/labstack/echo/v4" ) const sessionName = "zdravko-hey" @@ -148,30 +148,33 @@ func (h *BaseHandler) ClearAuthenticatedUserForRequest(w http.ResponseWriter, r type AuthenticatedHandler func(http.ResponseWriter, *http.Request, *AuthenticatedPrincipal) -func (h *BaseHandler) Authenticated(next AuthenticatedHandler) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { +type AuthenticatedContext struct { + echo.Context + Principal *AuthenticatedPrincipal +} + +func (h *BaseHandler) Authenticated(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { // First try cookie authentication - user, err := h.AuthenticateRequestWithCookies(r) + user, err := h.AuthenticateRequestWithCookies(c.Request()) if err == nil { if user.OAuth2Expiry.Before(time.Now()) { - user, err = h.RefreshToken(w, r, user) + user, err = h.RefreshToken(c.Response(), c.Request(), user) if err != nil { - http.Redirect(w, r, "/oauth2/login", http.StatusTemporaryRedirect) - return + return c.Redirect(http.StatusTemporaryRedirect, "/oauth2/login") } } - next(w, r, &AuthenticatedPrincipal{user, nil}) - return + + cc := AuthenticatedContext{c, &AuthenticatedPrincipal{user, nil}} + return next(cc) } // Then try token based authentication - principal, err := h.AuthenticateRequestWithToken(r) + principal, err := h.AuthenticateRequestWithToken(c.Request()) if err == nil { - next(w, r, principal) - return + cc := AuthenticatedContext{c, principal} + return next(cc) } - log.Println("err: ", err) - - http.Redirect(w, r, "/oauth2/login", http.StatusTemporaryRedirect) + return c.Redirect(http.StatusTemporaryRedirect, "/oauth2/login") } } diff --git a/internal/handlers/index.go b/internal/handlers/index.go index 3baf6bc..81e452a 100644 --- a/internal/handlers/index.go +++ b/internal/handlers/index.go @@ -3,10 +3,9 @@ package handlers import ( "math/rand" "net/http" - "text/template" - "code.tjo.space/mentos1386/zdravko/web/templates" "code.tjo.space/mentos1386/zdravko/web/templates/components" + "github.com/labstack/echo/v4" ) type IndexData struct { @@ -39,17 +38,8 @@ func newMockHealthCheck(domain string) *HealthCheck { } } -func (h *BaseHandler) Index(w http.ResponseWriter, r *http.Request) { - ts, err := template.ParseFS(templates.Templates, - "components/base.tmpl", - "pages/index.tmpl", - ) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - err = ts.ExecuteTemplate(w, "base", &IndexData{ +func (h *BaseHandler) Index(c echo.Context) error { + return c.Render(http.StatusOK, "index.tmpl", &IndexData{ Base: &components.Base{ NavbarActive: GetPageByTitle(Pages, "Status"), Navbar: Pages, @@ -61,7 +51,4 @@ func (h *BaseHandler) Index(w http.ResponseWriter, r *http.Request) { newMockHealthCheck("foo.example.net"), }, }) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } } diff --git a/internal/handlers/oauth2.go b/internal/handlers/oauth2.go index 893f702..2c9da8d 100644 --- a/internal/handlers/oauth2.go +++ b/internal/handlers/oauth2.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -13,6 +14,7 @@ import ( "code.tjo.space/mentos1386/zdravko/internal/config" "code.tjo.space/mentos1386/zdravko/internal/models" + "github.com/labstack/echo/v4" "golang.org/x/oauth2" ) @@ -79,64 +81,60 @@ func (h *BaseHandler) RefreshToken(w http.ResponseWriter, r *http.Request, user return refreshedUser, nil } -func (h *BaseHandler) OAuth2LoginGET(w http.ResponseWriter, r *http.Request) { +func (h *BaseHandler) OAuth2LoginGET(c echo.Context) error { conf := newOAuth2(h.config) state := newRandomState() result := h.db.Create(&models.OAuth2State{State: state, Expiry: time.Now().Add(5 * time.Minute)}) if result.Error != nil { - http.Error(w, result.Error.Error(), http.StatusInternalServerError) + return result.Error } url := conf.AuthCodeURL(state, oauth2.AccessTypeOffline) - http.Redirect(w, r, url, http.StatusTemporaryRedirect) + return c.Redirect(http.StatusTemporaryRedirect, url) } -func (h *BaseHandler) OAuth2CallbackGET(w http.ResponseWriter, r *http.Request) { +func (h *BaseHandler) OAuth2CallbackGET(c echo.Context) error { ctx := context.Background() conf := newOAuth2(h.config) - state := r.URL.Query().Get("state") + state := c.QueryParam("state") + code := c.QueryParam("code") result, err := h.query.OAuth2State.WithContext(ctx).Where( h.query.OAuth2State.State.Eq(state), h.query.OAuth2State.Expiry.Gt(time.Now()), ).Delete() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } if result.RowsAffected != 1 { - http.Error(w, "Invalid state", http.StatusUnauthorized) - return + return errors.New("invalid state") } // Exchange the code for a new token. - tok, err := conf.Exchange(r.Context(), r.URL.Query().Get("code")) + tok, err := conf.Exchange(ctx, code) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } // Ge the user information. client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(tok)) resp, err := client.Get(h.config.OAuth2.EndpointUserInfoURL) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } var userInfo UserInfo err = json.Unmarshal(body, &userInfo) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } userId := userInfo.Sub @@ -144,7 +142,7 @@ func (h *BaseHandler) OAuth2CallbackGET(w http.ResponseWriter, r *http.Request) userId = strconv.Itoa(userInfo.Id) } - err = h.SetAuthenticatedUserForRequest(w, r, &AuthenticatedUser{ + err = h.SetAuthenticatedUserForRequest(c.Response(), c.Request(), &AuthenticatedUser{ ID: userId, Email: userInfo.Email, OAuth2AccessToken: tok.AccessToken, @@ -153,27 +151,28 @@ func (h *BaseHandler) OAuth2CallbackGET(w http.ResponseWriter, r *http.Request) OAuth2Expiry: tok.Expiry, }) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } - http.Redirect(w, r, "/settings", http.StatusTemporaryRedirect) + return c.Redirect(http.StatusTemporaryRedirect, "/settings") } -func (h *BaseHandler) OAuth2LogoutGET(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { +func (h *BaseHandler) OAuth2LogoutGET(c echo.Context) error { + cc := c.(AuthenticatedContext) + if h.config.OAuth2.EndpointLogoutURL != "" { - tok := h.AuthenticatedUserToOAuth2Token(principal.User) + tok := h.AuthenticatedUserToOAuth2Token(cc.Principal.User) client := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(tok)) _, err := client.Get(h.config.OAuth2.EndpointLogoutURL) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } } - err := h.ClearAuthenticatedUserForRequest(w, r) + err := h.ClearAuthenticatedUserForRequest(c.Response(), c.Request()) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + + return c.Redirect(http.StatusTemporaryRedirect, "/") } diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go index 601cd0f..a42e31b 100644 --- a/internal/handlers/settings.go +++ b/internal/handlers/settings.go @@ -2,10 +2,9 @@ package handlers import ( "net/http" - "text/template" - "code.tjo.space/mentos1386/zdravko/web/templates" "code.tjo.space/mentos1386/zdravko/web/templates/components" + "github.com/labstack/echo/v4" ) type Settings struct { @@ -49,23 +48,12 @@ var SettingsNavbar = []*components.Page{ GetPageByTitle(SettingsPages, "Logout"), } -func (h *BaseHandler) SettingsOverviewGET(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { - ts, err := template.ParseFS(templates.Templates, - "components/base.tmpl", - "components/settings.tmpl", - "pages/settings_overview.tmpl", - ) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } +func (h *BaseHandler) SettingsOverviewGET(c echo.Context) error { + cc := c.(AuthenticatedContext) - err = ts.ExecuteTemplate(w, "base", NewSettings( - principal.User, + return c.Render(http.StatusOK, "settings_overview.tmpl", NewSettings( + cc.Principal.User, GetPageByTitle(SettingsPages, "Overview"), []*components.Page{GetPageByTitle(SettingsPages, "Overview")}, )) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } } diff --git a/internal/handlers/settingshealthchecks.go b/internal/handlers/settingshealthchecks.go index e37a65d..6e5da81 100644 --- a/internal/handlers/settingshealthchecks.go +++ b/internal/handlers/settingshealthchecks.go @@ -5,15 +5,13 @@ import ( "fmt" "net/http" "strings" - "text/template" "code.tjo.space/mentos1386/zdravko/internal/models" "code.tjo.space/mentos1386/zdravko/internal/services" - "code.tjo.space/mentos1386/zdravko/web/templates" "code.tjo.space/mentos1386/zdravko/web/templates/components" "github.com/go-playground/validator/v10" - "github.com/gorilla/mux" "github.com/gosimple/slug" + "github.com/labstack/echo/v4" ) type SettingsHealthchecks struct { @@ -27,58 +25,38 @@ type SettingsHealthcheck struct { Healthcheck *models.HealthcheckHttp } -func (h *BaseHandler) SettingsHealthchecksGET(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { - ts, err := template.ParseFS(templates.Templates, - "components/base.tmpl", - "components/settings.tmpl", - "pages/settings_healthchecks.tmpl", - ) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } +func (h *BaseHandler) SettingsHealthchecksGET(c echo.Context) error { + cc := c.(AuthenticatedContext) healthchecks, err := h.query.HealthcheckHttp.WithContext(context.Background()).Find() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } - err = ts.ExecuteTemplate(w, "base", &SettingsHealthchecks{ + return c.Render(http.StatusOK, "settings_healthchecks.tmpl", &SettingsHealthchecks{ Settings: NewSettings( - principal.User, + cc.Principal.User, GetPageByTitle(SettingsPages, "Healthchecks"), []*components.Page{GetPageByTitle(SettingsPages, "Healthchecks")}, ), Healthchecks: healthchecks, HealthchecksLength: len(healthchecks), }) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } } -func (h *BaseHandler) SettingsHealthchecksDescribeGET(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { - vars := mux.Vars(r) - slug := vars["slug"] +func (h *BaseHandler) SettingsHealthchecksDescribeGET(c echo.Context) error { + cc := c.(AuthenticatedContext) - ts, err := template.ParseFS(templates.Templates, - "components/base.tmpl", - "components/settings.tmpl", - "pages/settings_healthchecks_describe.tmpl", - ) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + slug := c.Param("slug") healthcheck, err := services.GetHealthcheckHttp(context.Background(), h.query, slug) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } - err = ts.ExecuteTemplate(w, "base", &SettingsHealthcheck{ + return c.Render(http.StatusOK, "settings_healthchecks_describe.tmpl", &SettingsHealthcheck{ Settings: NewSettings( - principal.User, + cc.Principal.User, GetPageByTitle(SettingsPages, "Healthchecks"), []*components.Page{ GetPageByTitle(SettingsPages, "Healthchecks"), @@ -90,52 +68,38 @@ func (h *BaseHandler) SettingsHealthchecksDescribeGET(w http.ResponseWriter, r * }), Healthcheck: healthcheck, }) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } } -func (h *BaseHandler) SettingsHealthchecksCreateGET(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { - ts, err := template.ParseFS(templates.Templates, - "components/base.tmpl", - "components/settings.tmpl", - "pages/settings_healthchecks_create.tmpl", - ) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } +func (h *BaseHandler) SettingsHealthchecksCreateGET(c echo.Context) error { + cc := c.(AuthenticatedContext) - err = ts.ExecuteTemplate(w, "base", NewSettings( - principal.User, + return c.Render(http.StatusOK, "settings_healthchecks_create.tmpl", NewSettings( + cc.Principal.User, GetPageByTitle(SettingsPages, "Healthchecks"), []*components.Page{ GetPageByTitle(SettingsPages, "Healthchecks"), GetPageByTitle(SettingsPages, "Healthchecks Create"), }, )) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } } -func (h *BaseHandler) SettingsHealthchecksCreatePOST(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { +func (h *BaseHandler) SettingsHealthchecksCreatePOST(c echo.Context) error { ctx := context.Background() healthcheckHttp := &models.HealthcheckHttp{ Healthcheck: models.Healthcheck{ - Name: r.FormValue("name"), - Slug: slug.Make(r.FormValue("name")), - Schedule: r.FormValue("schedule"), - WorkerGroups: strings.Split(r.FormValue("workergroups"), ","), + Name: c.FormValue("name"), + Slug: slug.Make(c.FormValue("name")), + Schedule: c.FormValue("schedule"), + WorkerGroups: strings.Split(c.FormValue("workergroups"), ","), }, - Url: r.FormValue("url"), - Method: r.FormValue("method"), + Url: c.FormValue("url"), + Method: c.FormValue("method"), } err := validator.New(validator.WithRequiredStructEnabled()).Struct(healthcheckHttp) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } err = services.CreateHealthcheckHttp( @@ -144,13 +108,13 @@ func (h *BaseHandler) SettingsHealthchecksCreatePOST(w http.ResponseWriter, r *h healthcheckHttp, ) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } err = services.StartHealthcheckHttp(ctx, h.temporal, healthcheckHttp) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } - http.Redirect(w, r, "/settings/healthchecks", http.StatusSeeOther) + return c.Redirect(http.StatusSeeOther, "/settings/healthchecks") } diff --git a/internal/handlers/settingsworkers.go b/internal/handlers/settingsworkers.go index 26a07a6..459358b 100644 --- a/internal/handlers/settingsworkers.go +++ b/internal/handlers/settingsworkers.go @@ -4,16 +4,14 @@ import ( "context" "fmt" "net/http" - "text/template" "code.tjo.space/mentos1386/zdravko/internal/jwt" "code.tjo.space/mentos1386/zdravko/internal/models" "code.tjo.space/mentos1386/zdravko/internal/services" - "code.tjo.space/mentos1386/zdravko/web/templates" "code.tjo.space/mentos1386/zdravko/web/templates/components" "github.com/go-playground/validator/v10" - "github.com/gorilla/mux" "github.com/gosimple/slug" + "github.com/labstack/echo/v4" ) type SettingsWorkers struct { @@ -27,58 +25,38 @@ type SettingsWorker struct { Worker *models.Worker } -func (h *BaseHandler) SettingsWorkersGET(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { - ts, err := template.ParseFS(templates.Templates, - "components/base.tmpl", - "components/settings.tmpl", - "pages/settings_workers.tmpl", - ) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } +func (h *BaseHandler) SettingsWorkersGET(c echo.Context) error { + cc := c.(AuthenticatedContext) workers, err := h.query.Worker.WithContext(context.Background()).Find() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } - err = ts.ExecuteTemplate(w, "base", &SettingsWorkers{ + return c.Render(http.StatusOK, "settings_workers.tmpl", &SettingsWorkers{ Settings: NewSettings( - principal.User, + cc.Principal.User, GetPageByTitle(SettingsPages, "Workers"), []*components.Page{GetPageByTitle(SettingsPages, "Workers")}, ), Workers: workers, WorkersLength: len(workers), }) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } } -func (h *BaseHandler) SettingsWorkersDescribeGET(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { - vars := mux.Vars(r) - slug := vars["slug"] +func (h *BaseHandler) SettingsWorkersDescribeGET(c echo.Context) error { + cc := c.(AuthenticatedContext) - ts, err := template.ParseFS(templates.Templates, - "components/base.tmpl", - "components/settings.tmpl", - "pages/settings_workers_describe.tmpl", - ) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + slug := c.Param("slug") worker, err := services.GetWorker(context.Background(), h.query, slug) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } - err = ts.ExecuteTemplate(w, "base", &SettingsWorker{ + return c.Render(http.StatusOK, "setting_workers_describe.tmpl", &SettingsWorker{ Settings: NewSettings( - principal.User, + cc.Principal.User, GetPageByTitle(SettingsPages, "Workers"), []*components.Page{ GetPageByTitle(SettingsPages, "Workers"), @@ -90,47 +68,33 @@ func (h *BaseHandler) SettingsWorkersDescribeGET(w http.ResponseWriter, r *http. }), Worker: worker, }) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } } -func (h *BaseHandler) SettingsWorkersCreateGET(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { - ts, err := template.ParseFS(templates.Templates, - "components/base.tmpl", - "components/settings.tmpl", - "pages/settings_workers_create.tmpl", - ) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } +func (h *BaseHandler) SettingsWorkersCreateGET(c echo.Context) error { + cc := c.(AuthenticatedContext) - err = ts.ExecuteTemplate(w, "base", NewSettings( - principal.User, + return c.Render(http.StatusOK, "settings_workers_create.tmpl", NewSettings( + cc.Principal.User, GetPageByTitle(SettingsPages, "Workers"), []*components.Page{ GetPageByTitle(SettingsPages, "Workers"), GetPageByTitle(SettingsPages, "Workers Create"), }, )) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } } -func (h *BaseHandler) SettingsWorkersCreatePOST(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { +func (h *BaseHandler) SettingsWorkersCreatePOST(c echo.Context) error { ctx := context.Background() worker := &models.Worker{ - Name: r.FormValue("name"), - Slug: slug.Make(r.FormValue("name")), - Group: r.FormValue("group"), + Name: c.FormValue("name"), + Slug: slug.Make(c.FormValue("name")), + Group: c.FormValue("group"), } err := validator.New(validator.WithRequiredStructEnabled()).Struct(worker) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } err = services.CreateWorker( @@ -139,27 +103,25 @@ func (h *BaseHandler) SettingsWorkersCreatePOST(w http.ResponseWriter, r *http.R worker, ) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } - http.Redirect(w, r, "/settings/workers", http.StatusSeeOther) + return c.Redirect(http.StatusSeeOther, "/settings/workers") } -func (h *BaseHandler) SettingsWorkersTokenGET(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { - vars := mux.Vars(r) - slug := vars["slug"] +func (h *BaseHandler) SettingsWorkersTokenGET(c echo.Context) error { + slug := c.Param("slug") worker, err := services.GetWorker(context.Background(), h.query, slug) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } // Allow write access to default namespace token, err := jwt.NewTokenForWorker(h.config.Jwt.PrivateKey, h.config.Jwt.PublicKey, worker) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"token": "` + token + `"}`)) + return c.JSON(http.StatusOK, map[string]string{"token": token}) } diff --git a/internal/handlers/temporal.go b/internal/handlers/temporal.go index 440ab1d..3aa1e59 100644 --- a/internal/handlers/temporal.go +++ b/internal/handlers/temporal.go @@ -6,9 +6,12 @@ import ( "net/url" "code.tjo.space/mentos1386/zdravko/internal/jwt" + "github.com/labstack/echo/v4" ) -func (h *BaseHandler) Temporal(w http.ResponseWriter, r *http.Request, principal *AuthenticatedPrincipal) { +func (h *BaseHandler) Temporal(c echo.Context) error { + cc := c.(AuthenticatedContext) + proxy := httputil.NewSingleHostReverseProxy(&url.URL{ Host: h.config.Temporal.UIHost, Scheme: "http", @@ -23,10 +26,11 @@ func (h *BaseHandler) Temporal(w http.ResponseWriter, r *http.Request, principal token, _ := jwt.NewTokenForUser( h.config.Jwt.PrivateKey, h.config.Jwt.PublicKey, - principal.User.Email, + cc.Principal.User.Email, ) r.Header.Add("Authorization", "Bearer "+token) } - proxy.ServeHTTP(w, r) + proxy.ServeHTTP(c.Response(), c.Request()) + return nil } diff --git a/internal/models/models.go b/internal/models/models.go index 7d50569..db11c0d 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -35,6 +35,8 @@ type HealthcheckHttp struct { Healthcheck Url string `validate:"required,url"` Method string `validate:"required,oneof=GET POST"` + + History []HealthcheckHttpHistory `gorm:"foreignKey:ID"` } type HealthcheckTcp struct { diff --git a/internal/models/query/healthcheck_http_histories.gen.go b/internal/models/query/healthcheck_http_histories.gen.go index b77850d..ce7dadb 100644 --- a/internal/models/query/healthcheck_http_histories.gen.go +++ b/internal/models/query/healthcheck_http_histories.gen.go @@ -36,6 +36,19 @@ func newHealthcheckHttpHistory(db *gorm.DB, opts ...gen.DOOption) healthcheckHtt db: db.Session(&gorm.Session{}), RelationField: field.NewRelation("HealthcheckHTTP", "models.HealthcheckHttp"), + History: struct { + field.RelationField + HealthcheckHTTP struct { + field.RelationField + } + }{ + RelationField: field.NewRelation("HealthcheckHTTP.History", "models.HealthcheckHttpHistory"), + HealthcheckHTTP: struct { + field.RelationField + }{ + RelationField: field.NewRelation("HealthcheckHTTP.History.HealthcheckHTTP", "models.HealthcheckHttp"), + }, + }, } _healthcheckHttpHistory.fillFieldMap() @@ -125,6 +138,13 @@ type healthcheckHttpHistoryHasOneHealthcheckHTTP struct { db *gorm.DB field.RelationField + + History struct { + field.RelationField + HealthcheckHTTP struct { + field.RelationField + } + } } func (a healthcheckHttpHistoryHasOneHealthcheckHTTP) Where(conds ...field.Expr) *healthcheckHttpHistoryHasOneHealthcheckHTTP { diff --git a/internal/models/query/healthcheck_https.gen.go b/internal/models/query/healthcheck_https.gen.go index f53f30d..04b42fd 100644 --- a/internal/models/query/healthcheck_https.gen.go +++ b/internal/models/query/healthcheck_https.gen.go @@ -36,9 +36,27 @@ func newHealthcheckHttp(db *gorm.DB, opts ...gen.DOOption) healthcheckHttp { _healthcheckHttp.Status = field.NewString(tableName, "status") _healthcheckHttp.UptimePercentage = field.NewFloat64(tableName, "uptime_percentage") _healthcheckHttp.Schedule = field.NewString(tableName, "schedule") - _healthcheckHttp.Groups = field.NewField(tableName, "groups") + _healthcheckHttp.WorkerGroups = field.NewField(tableName, "worker_groups") _healthcheckHttp.Url = field.NewString(tableName, "url") _healthcheckHttp.Method = field.NewString(tableName, "method") + _healthcheckHttp.History = healthcheckHttpHasManyHistory{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("History", "models.HealthcheckHttpHistory"), + HealthcheckHTTP: struct { + field.RelationField + History struct { + field.RelationField + } + }{ + RelationField: field.NewRelation("History.HealthcheckHTTP", "models.HealthcheckHttp"), + History: struct { + field.RelationField + }{ + RelationField: field.NewRelation("History.HealthcheckHTTP.History", "models.HealthcheckHttpHistory"), + }, + }, + } _healthcheckHttp.fillFieldMap() @@ -58,9 +76,10 @@ type healthcheckHttp struct { Status field.String UptimePercentage field.Float64 Schedule field.String - Groups field.Field + WorkerGroups field.Field Url field.String Method field.String + History healthcheckHttpHasManyHistory fieldMap map[string]field.Expr } @@ -86,7 +105,7 @@ func (h *healthcheckHttp) updateTableName(table string) *healthcheckHttp { h.Status = field.NewString(table, "status") h.UptimePercentage = field.NewFloat64(table, "uptime_percentage") h.Schedule = field.NewString(table, "schedule") - h.Groups = field.NewField(table, "groups") + h.WorkerGroups = field.NewField(table, "worker_groups") h.Url = field.NewString(table, "url") h.Method = field.NewString(table, "method") @@ -117,7 +136,7 @@ func (h *healthcheckHttp) GetFieldByName(fieldName string) (field.OrderExpr, boo } func (h *healthcheckHttp) fillFieldMap() { - h.fieldMap = make(map[string]field.Expr, 12) + h.fieldMap = make(map[string]field.Expr, 13) h.fieldMap["id"] = h.ID h.fieldMap["created_at"] = h.CreatedAt h.fieldMap["updated_at"] = h.UpdatedAt @@ -127,9 +146,10 @@ func (h *healthcheckHttp) fillFieldMap() { h.fieldMap["status"] = h.Status h.fieldMap["uptime_percentage"] = h.UptimePercentage h.fieldMap["schedule"] = h.Schedule - h.fieldMap["groups"] = h.Groups + h.fieldMap["worker_groups"] = h.WorkerGroups h.fieldMap["url"] = h.Url h.fieldMap["method"] = h.Method + } func (h healthcheckHttp) clone(db *gorm.DB) healthcheckHttp { @@ -142,6 +162,84 @@ func (h healthcheckHttp) replaceDB(db *gorm.DB) healthcheckHttp { return h } +type healthcheckHttpHasManyHistory struct { + db *gorm.DB + + field.RelationField + + HealthcheckHTTP struct { + field.RelationField + History struct { + field.RelationField + } + } +} + +func (a healthcheckHttpHasManyHistory) Where(conds ...field.Expr) *healthcheckHttpHasManyHistory { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a healthcheckHttpHasManyHistory) WithContext(ctx context.Context) *healthcheckHttpHasManyHistory { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a healthcheckHttpHasManyHistory) Session(session *gorm.Session) *healthcheckHttpHasManyHistory { + a.db = a.db.Session(session) + return &a +} + +func (a healthcheckHttpHasManyHistory) Model(m *models.HealthcheckHttp) *healthcheckHttpHasManyHistoryTx { + return &healthcheckHttpHasManyHistoryTx{a.db.Model(m).Association(a.Name())} +} + +type healthcheckHttpHasManyHistoryTx struct{ tx *gorm.Association } + +func (a healthcheckHttpHasManyHistoryTx) Find() (result []*models.HealthcheckHttpHistory, err error) { + return result, a.tx.Find(&result) +} + +func (a healthcheckHttpHasManyHistoryTx) Append(values ...*models.HealthcheckHttpHistory) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a healthcheckHttpHasManyHistoryTx) Replace(values ...*models.HealthcheckHttpHistory) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a healthcheckHttpHasManyHistoryTx) Delete(values ...*models.HealthcheckHttpHistory) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a healthcheckHttpHasManyHistoryTx) Clear() error { + return a.tx.Clear() +} + +func (a healthcheckHttpHasManyHistoryTx) Count() int64 { + return a.tx.Count() +} + type healthcheckHttpDo struct{ gen.DO } type IHealthcheckHttpDo interface { diff --git a/internal/models/query/healthcheck_tcps.gen.go b/internal/models/query/healthcheck_tcps.gen.go index 86993ac..2c1859a 100644 --- a/internal/models/query/healthcheck_tcps.gen.go +++ b/internal/models/query/healthcheck_tcps.gen.go @@ -36,7 +36,7 @@ func newHealthcheckTcp(db *gorm.DB, opts ...gen.DOOption) healthcheckTcp { _healthcheckTcp.Status = field.NewString(tableName, "status") _healthcheckTcp.UptimePercentage = field.NewFloat64(tableName, "uptime_percentage") _healthcheckTcp.Schedule = field.NewString(tableName, "schedule") - _healthcheckTcp.Groups = field.NewField(tableName, "groups") + _healthcheckTcp.WorkerGroups = field.NewField(tableName, "worker_groups") _healthcheckTcp.Hostname = field.NewString(tableName, "hostname") _healthcheckTcp.Port = field.NewInt(tableName, "port") @@ -58,7 +58,7 @@ type healthcheckTcp struct { Status field.String UptimePercentage field.Float64 Schedule field.String - Groups field.Field + WorkerGroups field.Field Hostname field.String Port field.Int @@ -86,7 +86,7 @@ func (h *healthcheckTcp) updateTableName(table string) *healthcheckTcp { h.Status = field.NewString(table, "status") h.UptimePercentage = field.NewFloat64(table, "uptime_percentage") h.Schedule = field.NewString(table, "schedule") - h.Groups = field.NewField(table, "groups") + h.WorkerGroups = field.NewField(table, "worker_groups") h.Hostname = field.NewString(table, "hostname") h.Port = field.NewInt(table, "port") @@ -127,7 +127,7 @@ func (h *healthcheckTcp) fillFieldMap() { h.fieldMap["status"] = h.Status h.fieldMap["uptime_percentage"] = h.UptimePercentage h.fieldMap["schedule"] = h.Schedule - h.fieldMap["groups"] = h.Groups + h.fieldMap["worker_groups"] = h.WorkerGroups h.fieldMap["hostname"] = h.Hostname h.fieldMap["port"] = h.Port } diff --git a/internal/services/healthcheck.go b/internal/services/healthcheck.go index 177e920..62973c0 100644 --- a/internal/services/healthcheck.go +++ b/internal/services/healthcheck.go @@ -29,14 +29,16 @@ func StartHealthcheckHttp(ctx context.Context, t client.Client, healthcheckHttp args := make([]interface{}, 0) args = append(args, workflows.HealthcheckHttpWorkflowParam{Url: healthcheckHttp.Url, Method: healthcheckHttp.Method}) + id := "healthcheck-http-" + healthcheckHttp.Slug + for _, group := range healthcheckHttp.WorkerGroups { _, err := t.ScheduleClient().Create(ctx, client.ScheduleOptions{ - ID: "healthcheck-http-" + healthcheckHttp.Slug, + ID: id + "-" + group, Spec: client.ScheduleSpec{ CronExpressions: []string{healthcheckHttp.Schedule}, }, Action: &client.ScheduleWorkflowAction{ - ID: "healthcheck-http-" + healthcheckHttp.Slug, + ID: id + "-" + group, Workflow: workflows.HealthcheckHttpWorkflowDefinition, Args: args, TaskQueue: group, @@ -52,3 +54,7 @@ func StartHealthcheckHttp(ctx context.Context, t client.Client, healthcheckHttp return nil } + +func CreateHealthcheckHistory(ctx context.Context, db *gorm.DB, healthcheckHistory *models.HealthcheckHttpHistory) error { + return db.WithContext(ctx).Create(healthcheckHistory).Error +} diff --git a/justfile b/justfile index 2d24def..e90a3d3 100644 --- a/justfile +++ b/justfile @@ -26,7 +26,12 @@ run-worker: # Start server run-server: go build -o dist/zdravko cmd/zdravko/main.go - ./dist/zdravko --server --temporal + ./dist/zdravko --server + +# Start temporal +run-temporal: + go build -o dist/zdravko cmd/zdravko/main.go + ./dist/zdravko --temporal # Generates new jwt key pair generate-jwt-key: diff --git a/pkg/server/server.go b/pkg/server/server.go index 2caba2f..af431c4 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -10,7 +10,9 @@ import ( "code.tjo.space/mentos1386/zdravko/internal/handlers" "code.tjo.space/mentos1386/zdravko/internal/temporal" "code.tjo.space/mentos1386/zdravko/web/static" - "github.com/gorilla/mux" + "code.tjo.space/mentos1386/zdravko/web/templates" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" ) type Server struct { @@ -29,7 +31,10 @@ func (s *Server) Name() string { } func (s *Server) Start() error { - r := mux.NewRouter() + e := echo.New() + e.Renderer = templates.NewTemplates() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) db, query, err := internal.ConnectToDatabase(s.cfg.DatabasePath) if err != nil { @@ -46,56 +51,70 @@ func (s *Server) Start() error { h := handlers.NewBaseHandler(db, query, temporalClient, s.cfg) // Health - r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + e.GET("/health", func(c echo.Context) error { d, err := db.DB() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } err = d.Ping() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - _, err = w.Write([]byte("OK")) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + return err } + + return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) }) // Server static files - r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(static.Static)))) + stat := e.Group("/static") + stat.Use(middleware.StaticWithConfig(middleware.StaticConfig{ + Filesystem: http.FS(static.Static), + })) - r.HandleFunc("/", h.Index).Methods("GET") + // Public + e.GET("", h.Index) // Settings - r.HandleFunc("/settings", h.Authenticated(h.SettingsOverviewGET)).Methods("GET") - r.HandleFunc("/settings/healthchecks", h.Authenticated(h.SettingsHealthchecksGET)).Methods("GET") - r.HandleFunc("/settings/healthchecks/create", h.Authenticated(h.SettingsHealthchecksCreateGET)).Methods("GET") - r.HandleFunc("/settings/healthchecks/create", h.Authenticated(h.SettingsHealthchecksCreatePOST)).Methods("POST") - r.HandleFunc("/settings/healthchecks/{slug}", h.Authenticated(h.SettingsHealthchecksDescribeGET)).Methods("GET") - r.HandleFunc("/settings/workers", h.Authenticated(h.SettingsWorkersGET)).Methods("GET") - r.HandleFunc("/settings/workers/create", h.Authenticated(h.SettingsWorkersCreateGET)).Methods("GET") - r.HandleFunc("/settings/workers/create", h.Authenticated(h.SettingsWorkersCreatePOST)).Methods("POST") - r.HandleFunc("/settings/workers/{slug}", h.Authenticated(h.SettingsWorkersDescribeGET)).Methods("GET") - r.HandleFunc("/settings/workers/{slug}/token", h.Authenticated(h.SettingsWorkersTokenGET)).Methods("GET") - r.PathPrefix("/settings/temporal").HandlerFunc(h.Authenticated(h.Temporal)) + settings := e.Group("/settings") + settings.Use(h.Authenticated) + settings.GET("", h.SettingsOverviewGET) + settings.GET("/healthchecks", h.SettingsHealthchecksGET) + settings.GET("/healthchecks/create", h.SettingsHealthchecksCreateGET) + settings.POST("/healthchecks/create", h.SettingsHealthchecksCreatePOST) + settings.GET("/healthchecks/:slug", h.SettingsHealthchecksDescribeGET) + settings.GET("/workers", h.SettingsWorkersGET) + settings.GET("/workers/create", h.SettingsWorkersCreateGET) + settings.POST("/workers/create", h.SettingsWorkersCreatePOST) + settings.GET("/workers/:slug", h.SettingsWorkersDescribeGET) + settings.GET("/workers/:slug/token", h.SettingsWorkersTokenGET) + settings.Match([]string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"}, "/temporal*", h.Temporal) // OAuth2 - r.HandleFunc("/oauth2/login", h.OAuth2LoginGET).Methods("GET") - r.HandleFunc("/oauth2/callback", h.OAuth2CallbackGET).Methods("GET") - r.HandleFunc("/oauth2/logout", h.Authenticated(h.OAuth2LogoutGET)).Methods("GET") + oauth2 := e.Group("/oauth2") + oauth2.GET("/login", h.OAuth2LoginGET) + oauth2.GET("/callback", h.OAuth2CallbackGET) + oauth2.GET("/logout", h.OAuth2LogoutGET, h.Authenticated) // API - r.HandleFunc("/api/v1/workers/connect", h.Authenticated(h.ApiV1WorkersConnectGET)).Methods("GET") + apiv1 := e.Group("/api/v1") + apiv1.Use(h.Authenticated) + apiv1.GET("/workers/connect", h.ApiV1WorkersConnectGET) + apiv1.POST("/healthcheck/:slug/history", h.ApiV1HealthchecksHistoryPOST) - // 404 - r.PathPrefix("/").HandlerFunc(h.Error404).Methods("GET") + // Error handler + e.HTTPErrorHandler = func(err error, c echo.Context) { + code := http.StatusInternalServerError + if he, ok := err.(*echo.HTTPError); ok { + code = he.Code + } - s.server = &http.Server{ - Addr: ":" + s.cfg.Port, - Handler: r, + if code == http.StatusNotFound { + _ = h.Error404(c) + return + } + _ = c.String(code, err.Error()) } - return s.server.ListenAndServe() + return e.Start(":" + s.cfg.Port) } func (s *Server) Stop() error { diff --git a/web/templates/tempaltes.go b/web/templates/tempaltes.go index 83a8320..c80ec0d 100644 --- a/web/templates/tempaltes.go +++ b/web/templates/tempaltes.go @@ -2,7 +2,57 @@ package templates import ( "embed" + "io" + "log" + "text/template" + + "github.com/labstack/echo/v4" ) //go:embed * -var Templates embed.FS +var templates embed.FS + +const base = "components/base.tmpl" + +type Templates struct { + templates map[string]*template.Template +} + +func load(files ...string) *template.Template { + files = append(files, base) + return template.Must(template.ParseFS(templates, files...)) +} + +func loadSettings(files ...string) *template.Template { + files = append(files, "components/settings.tmpl") + return load(files...) +} + +func NewTemplates() *Templates { + return &Templates{ + templates: map[string]*template.Template{ + "404.tmpl": load("pages/404.tmpl"), + "index.tmpl": load("pages/index.tmpl"), + "settings_overview.tmpl": loadSettings("pages/settings_overview.tmpl"), + "settings_workers.tmpl": loadSettings("pages/settings_workers.tmpl"), + "settings_workers_create.tmpl": loadSettings("pages/settings_workers_create.tmpl"), + "settings_healthchecks.tmpl": loadSettings("pages/settings_healthchecks.tmpl"), + "settings_healthchecks_create.tmpl": loadSettings("pages/settings_healthchecks_create.tmpl"), + "settings_healthchecks_describe.tmpl": loadSettings("pages/settings_healthchecks_describe.tmpl"), + }, + } +} + +func (t *Templates) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + if t.templates[name] == nil { + log.Printf("template not found: %s", name) + return echo.ErrNotFound + } + + err := t.templates[name].ExecuteTemplate(w, "base", data) + if err != nil { + log.Printf("error rendering template: %s", err) + } + + return err +}