refactor: replace gorilla/mux with echo

This commit is contained in:
Tine 2024-02-20 11:24:04 +01:00
parent 85e7c2765a
commit 9848871630
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
19 changed files with 409 additions and 311 deletions

13
go.mod
View file

@ -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

19
go.sum
View file

@ -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=

View file

@ -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
}
}

View file

@ -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"})
}

View file

@ -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")
}
}

View file

@ -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)
}
}

View file

@ -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, "/")
}

View file

@ -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)
}
}

View file

@ -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")
}

View file

@ -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})
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}

View file

@ -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:

View file

@ -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 {

View file

@ -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
}