From 57eaeea310cd675ee8c8eb9506e4baa122a441b9 Mon Sep 17 00:00:00 2001 From: Miroslav Misek Date: Thu, 27 Mar 2025 09:19:08 +0100 Subject: [PATCH] Initial commit --- .idea/maf-webserver.iml | 8 +++ .idea/modules.xml | 8 +++ .idea/workspace.xml | 9 +++ common.go | 5 ++ config.go | 15 ++++ context.go | 149 ++++++++++++++++++++++++++++++++++++++++ dto.go | 70 +++++++++++++++++++ go.mod | 30 ++++++++ go.sum | 50 ++++++++++++++ group.go | 28 ++++++++ interfaces.go | 36 ++++++++++ module.go | 144 ++++++++++++++++++++++++++++++++++++++ module_config.go | 7 ++ renderer.go | 89 ++++++++++++++++++++++++ server.go | 115 +++++++++++++++++++++++++++++++ 15 files changed, 763 insertions(+) create mode 100644 .idea/maf-webserver.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/workspace.xml create mode 100644 common.go create mode 100644 config.go create mode 100644 context.go create mode 100644 dto.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 group.go create mode 100644 interfaces.go create mode 100644 module.go create mode 100644 module_config.go create mode 100644 renderer.go create mode 100644 server.go diff --git a/.idea/maf-webserver.iml b/.idea/maf-webserver.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/maf-webserver.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..95a8a34 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..0e0384f --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/common.go b/common.go new file mode 100644 index 0000000..6923d00 --- /dev/null +++ b/common.go @@ -0,0 +1,5 @@ +package webserver + +import "github.com/labstack/echo/v4" + +type MiddlewareFunc = echo.MiddlewareFunc diff --git a/config.go b/config.go new file mode 100644 index 0000000..b453a2f --- /dev/null +++ b/config.go @@ -0,0 +1,15 @@ +package webserver + +type Config struct { + Port uint16 `yaml:"port" envconfig:"PORT"` + BaseUrl string `yaml:"baseUrl" envconfig:"BASE_URL"` + ReloadTemplates bool `yaml:"reloadTemplates" envconfig:"RELOAD_TEMPLATES"` +} + +func NewConfig() *Config { + return &Config{ + Port: 8080, + BaseUrl: "", + ReloadTemplates: false, + } +} diff --git a/context.go b/context.go new file mode 100644 index 0000000..34d4a11 --- /dev/null +++ b/context.go @@ -0,0 +1,149 @@ +package webserver + +import ( + "errors" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "log/slog" + "net/http" +) + +type HandlerFunc func(c *Context) error + +type Context struct { + echo.Context + csrfConfig *middleware.CSRFConfig +} + +func CustomContext(csrfConfig *middleware.CSRFConfig) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + cc := &Context{ + Context: c, + csrfConfig: csrfConfig, + } + return next(cc) + } + } +} + +func (c *Context) View(code int, filename string, obj interface{}) error { + if mapContext, ok := obj.(map[string]interface{}); ok { + viewData := mapContext + viewData["request"] = c.Request() + viewData["csrf"] = c.CSRF() + obj = viewData + } + return c.Context.Render(code, filename, obj) +} + +func (c *Context) DtoError(err error) error { + + switch err.(type) { + case *NotFoundError: + return echo.NewHTTPError(http.StatusNotFound, "Not found") + case *InternalError: + return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error") + case *InvalidRequestError: + return echo.NewHTTPError(http.StatusBadRequest, "Bad request") + default: + return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error") + } + +} + +func (c *Context) NotFound() error { + return echo.NewHTTPError(http.StatusNotFound) +} + +func (c *Context) InternalServerError(err ...error) error { + if err != nil && len(err) > 0 { + slog.Error("", err) + } + return echo.NewHTTPError(http.StatusInternalServerError) +} + +func Handle(handler HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + return handler(c.(*Context)) + } +} + +func (c *Context) CSRF() string { + return c.Get(middleware.DefaultCSRFConfig.ContextKey).(string) +} + +func (c *Context) GetFormData() (map[string]string, error) { + + var err error + ret := make(map[string]string) + + //c.MultipartForm() + contentType := c.Request().Header.Get("Content-Type") + if contentType == "multipart/form-data" { + err = c.Request().ParseMultipartForm(4 * 1024 * 1024) + } else if contentType == "application/x-www-form-urlencoded" { + err = c.Request().ParseForm() + } else { + err = errors.New("unsupported content type") + } + + if err != nil { + return nil, err + } + for k, v := range c.Request().PostForm { + ret[k] = v[len(v)-1] + } + + return ret, nil +} + +/* +func requestQueryToFilter(c *Context, params map[string]string) (i2bolt.Filter, map[string]string) { + + var subFilters []i2bolt.Filter + filterParams := make(map[string]string) + + query := c.Request().URL.Query() + for param, fieldMethod := range params { + + if v, ok := query[param]; ok { + value := strings.TrimSpace(v[0]) + if value != "" { + + parts := strings.Split(fieldMethod, "__") + fieldName := parts[0] + method := "" + if len(parts) > 1 { + method = parts[1] + } + + var valueIntf interface{} + valueIntf = value + + if method == "is" { + valueIntf = value == "yes" + } + + fieldNames := strings.Split(fieldName, "|") + for _, name := range fieldNames { + subFilters = append(subFilters, i2bolt.Q(name, method, valueIntf)) + filterParams[param] = value + } + + } else { + filterParams[param] = "" + } + + } + + } + + var filter i2bolt.Filter + if len(subFilters) > 0 { + filter = i2bolt.And(subFilters...) + } + + return filter, filterParams +} +*/ diff --git a/dto.go b/dto.go new file mode 100644 index 0000000..6bc81f5 --- /dev/null +++ b/dto.go @@ -0,0 +1,70 @@ +package webserver + +import "fmt" + +type GateError struct { + Message string + Err error +} + +func (e *GateError) Error() string { + if e.Message != "" && e.Err != nil { + return fmt.Sprintf("%s: %v", e.Message, e.Err) + } else if e.Message != "" { + return e.Message + } else { + return e.Err.Error() + } +} + +type InputError struct { + GateError +} + +func NewInputError(message string, err error) *InputError { + return &InputError{ + GateError{ + Message: message, + Err: err, + }, + } +} + +type InternalError struct { + GateError +} + +func NewInternalError(message string, err error) *InternalError { + return &InternalError{ + GateError{ + Message: message, + Err: err, + }, + } +} + +type NotFoundError struct { + GateError +} + +func NewNotFoundError(message string, err error) *NotFoundError { + return &NotFoundError{ + GateError{ + Message: message, + Err: err, + }, + } +} + +type InvalidRequestError struct { + GateError +} + +func NewInvalidRequestError(message string, err error) *InvalidRequestError { + return &InvalidRequestError{ + GateError{ + Message: message, + Err: err, + }, + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2a37c86 --- /dev/null +++ b/go.mod @@ -0,0 +1,30 @@ +module netgarden.dev/netgarden/maf-webserver + +go 1.24.1 + +replace netgarden.dev/netgarden/maf => ../maf + +replace netgarden.dev/netgarden/mergefs => ../mergefs + +require ( + github.com/flosch/pongo2/v6 v6.0.0 + github.com/labstack/echo/v4 v4.13.3 + netgarden.dev/netgarden/maf v0.0.0-00010101000000-000000000000 + netgarden.dev/netgarden/mergefs v0.0.0-00010101000000-000000000000 +) + +require ( + github.com/elliotchance/orderedmap v1.6.0 // indirect + github.com/kelseyhightower/envconfig v1.4.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.8.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..172b294 --- /dev/null +++ b/go.sum @@ -0,0 +1,50 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elliotchance/orderedmap v1.6.0 h1:xjn+kbbKXeDq6v9RVE+WYwRbYfAZKvlWfcJNxM8pvEw= +github.com/elliotchance/orderedmap v1.6.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= +github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= +github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +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/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.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= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/group.go b/group.go new file mode 100644 index 0000000..0f98ae9 --- /dev/null +++ b/group.go @@ -0,0 +1,28 @@ +package webserver + +import ( + "github.com/labstack/echo/v4" + "strings" +) + +type echoGroup = echo.Group +type Group struct { + *echoGroup +} + +func (g *Group) Group(prefix string, middleware ...MiddlewareFunc) *Group { + return &Group{ + g.echoGroup.Group(prefix, middleware...), + } +} + +func (g *Group) AddMany(methods string, path string, handler echo.HandlerFunc) { + + for _, method := range strings.Split(methods, " ") { + method = strings.TrimSpace(method) + if method != "" { + g.Add(method, path, handler) + } + } + +} diff --git a/interfaces.go b/interfaces.go new file mode 100644 index 0000000..f2e8b30 --- /dev/null +++ b/interfaces.go @@ -0,0 +1,36 @@ +package webserver + +import ( + "github.com/flosch/pongo2/v6" + "io/fs" + "netgarden.dev/netgarden/maf" +) + +type Controller interface { + Register(app *Group) +} + +type WebModule interface { + maf.Module + GetWebControllers() []Controller +} + +type WebModuleStaticFSProvider interface { + WebModule + GetWebStaticFS() fs.FS +} + +type WebModuleTemplatesFSProvider interface { + WebModule + GetWebTemplatesFS() fs.FS +} + +type WebModuleTemplateTagsProvider interface { + WebModule + GetWebTemplateTags(renderer *Renderer) map[string]pongo2.TagParser +} + +type WebModuleTemplateFiltersProvider interface { + WebModule + GetWebTemplateFilters() map[string]pongo2.FilterFunction +} diff --git a/module.go b/module.go new file mode 100644 index 0000000..5d65cfd --- /dev/null +++ b/module.go @@ -0,0 +1,144 @@ +package webserver + +import ( + "github.com/flosch/pongo2/v6" + "io/fs" + "netgarden.dev/netgarden/maf" + "netgarden.dev/netgarden/mergefs" +) + +func NewModule(moduleConfig *ModuleConfig) *Module { + return &Module{ + moduleConfig: moduleConfig, + } +} + +type Module struct { + moduleConfig *ModuleConfig + manager *maf.Manager + config *Config + server *Server +} + +func (m *Module) GetID() string { + return "webserver" +} + +func (m *Module) GetName() string { + return "webserver" +} + +func (m *Module) SetManager(manager *maf.Manager) { + m.manager = manager +} + +func (m *Module) CreateConfig() interface{} { + return NewConfig() +} + +func (m *Module) SetConfig(cfg interface{}) { + m.config = cfg.(*Config) +} + +func (m *Module) GetConfig() *Config { + return m.config +} + +func (m *Module) Initialize() error { + return nil +} + +func (m *Module) PreStart() error { + + m.server = NewServer(m) + + for _, module := range m.manager.GetModulesList() { + + webModule, ok := module.(WebModule) + if !ok { + continue + } + + if tagsProvider, ok := module.(WebModuleTemplateTagsProvider); ok { + for name, tag := range tagsProvider.GetWebTemplateTags(m.server.GetRenderer()) { + err := pongo2.RegisterTag(name, tag) + if err != nil { + return err + } + } + } + + if filtersProvider, ok := module.(WebModuleTemplateFiltersProvider); ok { + for name, filter := range filtersProvider.GetWebTemplateFilters() { + err := pongo2.RegisterFilter(name, filter) + if err != nil { + return err + } + } + } + + routerGroup := m.server.GetGroup() + controllers := webModule.GetWebControllers() + for _, controller := range controllers { + controller.Register(routerGroup) + } + + } + + return nil +} + +func (m *Module) Start() error { + return m.server.Start() +} + +func (m *Module) Stop() error { + if m.server == nil { + return nil + } + return m.server.Stop() +} + +func (m *Module) getStaticFS() fs.FS { + + fsList := make([]fs.FS, 0) + + for _, module := range m.manager.GetModulesList() { + + webModule, ok := module.(WebModuleStaticFSProvider) + if !ok { + continue + } + + tplFS := webModule.GetWebStaticFS() + if tplFS == nil { + continue + } + + fsList = append(fsList, tplFS) + } + + return mergefs.Merge(fsList...) +} + +func (m *Module) getTemplatesFS() fs.FS { + + fsList := make([]fs.FS, 0) + + for _, module := range m.manager.GetModulesList() { + + webModule, ok := module.(WebModuleTemplatesFSProvider) + if !ok { + continue + } + + tplFS := webModule.GetWebTemplatesFS() + if tplFS == nil { + continue + } + + fsList = append(fsList, tplFS) + } + + return mergefs.Merge(fsList...) +} diff --git a/module_config.go b/module_config.go new file mode 100644 index 0000000..57b9e15 --- /dev/null +++ b/module_config.go @@ -0,0 +1,7 @@ +package webserver + +type MiddlewareFuncProvider = func() MiddlewareFunc + +type ModuleConfig struct { + Middlewares []MiddlewareFuncProvider +} diff --git a/renderer.go b/renderer.go new file mode 100644 index 0000000..dc886d4 --- /dev/null +++ b/renderer.go @@ -0,0 +1,89 @@ +package webserver + +import ( + "bytes" + "errors" + "fmt" + "github.com/flosch/pongo2/v6" + "github.com/labstack/echo/v4" + "io" + "io/fs" +) + +type Renderer struct { + fs fs.FS + templates *pongo2.TemplateSet +} + +// NewRenderer creates a new Renderer struct. +func NewRenderer(fs fs.FS) *Renderer { + + r := &Renderer{ + fs: fs, + } + + templates := pongo2.NewSet("templates", r) + r.templates = templates + + return r +} + +// Abs returns absolute path to file requested. +// Search path is configured in AddDirectory method. +// And default directory is "./templates". +func (p *Renderer) Abs(base, name string) string { + return name +} + +// Get reads the path's content from your local filesystem. +func (p *Renderer) Get(path string) (io.Reader, error) { + + f, err := p.fs.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + data, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + return bytes.NewReader(data), nil +} + +// RegisterTag registers a custom tag. +// It calls pongo2.RegisterTag method. +func (p *Renderer) RegisterTag(name string, parserFunc pongo2.TagParser) { + pongo2.RegisterTag(name, parserFunc) +} + +// RegisterFilter registers a custom filter. +// It calls pongo2.RegisterFilter method. +func (p *Renderer) RegisterFilter(name string, fn pongo2.FilterFunction) { + pongo2.RegisterFilter(name, fn) +} + +// SetDebug sets debug mode to the template set. +// See pongo2.TemplateSet.Debug for more information. +func (p *Renderer) SetDebug(v bool) { + p.templates.Debug = v +} + +// Render renders the view. +// Many other times, this is called in your echo handler functions. +func (p *Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + tmpl, err := p.templates.FromCache(name) + if err != nil { + return err + } + if tmpl == nil { + return fmt.Errorf("template '%s' not found", name) + } + d, ok := data.(map[string]interface{}) + if !ok { + return errors.New("Incorrect data format. Should be map[string]interface{}") + } + + return tmpl.ExecuteWriter(d, w) +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..3805c4f --- /dev/null +++ b/server.go @@ -0,0 +1,115 @@ +package webserver + +import ( + "context" + "fmt" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "log/slog" + "net" + "net/http" + "syscall" + "time" +) + +type Server struct { + module *Module + listener *net.Listener + csrfConfig *middleware.CSRFConfig + router *echo.Echo + server *http.Server +} + +func NewServer(module *Module) *Server { + + s := &Server{ + module: module, + } + + s.init() + + return s +} + +func (s *Server) init() { + + s.csrfConfig = &middleware.CSRFConfig{ + TokenLookup: "form:_csrf,query:csrf,header:X-CSRF-Token", + } + + s.router = echo.New() + //s.router.Pre(middleware.RemoveTrailingSlash()) + s.router.Use(middleware.Logger()) + s.router.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ + LogLevel: 4, + })) + s.router.Use(middleware.CSRFWithConfig(*s.csrfConfig)) + s.router.Use(CustomContext(s.csrfConfig)) + if s.module.moduleConfig != nil && s.module.moduleConfig.Middlewares != nil { + for _, m := range s.module.moduleConfig.Middlewares { + s.router.Use(m()) + } + } + + tplFS := s.module.getTemplatesFS() + s.router.Renderer = NewRenderer(tplFS) + + s.router.StaticFS("/static", s.module.getStaticFS()) + +} + +func (s *Server) Start() error { + + config := &net.ListenConfig{Control: s.reusePort} + listener, err := config.Listen( + context.Background(), + "tcp", + fmt.Sprintf(":%d", s.module.config.Port), + ) + if err != nil { + return err + } + + s.listener = &listener + + s.server = &http.Server{ + Addr: fmt.Sprintf(":%d", s.module.config.Port), + Handler: s.router, + } + + go s.server.Serve(listener) + + return nil +} + +func (s *Server) Stop() error { + + if s.server == nil { + return nil + } + + timeout := 10 * time.Second + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + return s.server.Shutdown(ctx) +} + +func (s *Server) GetGroup() *Group { + return &Group{ + s.router.Group("/"), + } +} + +func (s *Server) GetRenderer() *Renderer { + return s.router.Renderer.(*Renderer) +} + +func (s *Server) reusePort(network, address string, conn syscall.RawConn) error { + return conn.Control(func(descriptor uintptr) { + err := syscall.SetsockoptInt(int(descriptor), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + if err != nil { + slog.Error("error during setting reuseport", err) + } + }) +}