diff --git a/adapter/nethttp/nethttp.go b/adapter/nethttp/nethttp.go new file mode 100644 index 000000000..353c76bff --- /dev/null +++ b/adapter/nethttp/nethttp.go @@ -0,0 +1,236 @@ +package nethttp + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "net/url" + "regexp" + "strings" + + "github.com/GoAdminGroup/go-admin/adapter" + "github.com/GoAdminGroup/go-admin/context" + "github.com/GoAdminGroup/go-admin/engine" + cfg "github.com/GoAdminGroup/go-admin/modules/config" + "github.com/GoAdminGroup/go-admin/modules/constant" + "github.com/GoAdminGroup/go-admin/plugins" + "github.com/GoAdminGroup/go-admin/plugins/admin/models" + "github.com/GoAdminGroup/go-admin/template/types" +) + +type NetHTTP struct { + adapter.BaseAdapter + ctx Context + app *http.ServeMux +} + +func init() { + engine.Register(new(NetHTTP)) +} + +// User implements the method Adapter.User. +func (nh *NetHTTP) User(ctx interface{}) (models.UserModel, bool) { + return nh.GetUser(ctx, nh) +} + +// Use implements the method Adapter.Use. +func (nh *NetHTTP) Use(app interface{}, plugs []plugins.Plugin) error { + return nh.GetUse(app, plugs, nh) +} + +// Content implements the method Adapter.Content. +func (nh *NetHTTP) Content(ctx interface{}, getPanelFn types.GetPanelFn, fn context.NodeProcessor, btns ...types.Button) { + nh.GetContent(ctx, getPanelFn, nh, btns, fn) +} + +type HandlerFunc func(ctx Context) (types.Panel, error) + +func Content(handler HandlerFunc) http.HandlerFunc { + return func(writer http.ResponseWriter, request *http.Request) { + ctx := Context{ + Request: request, + Response: writer, + } + engine.Content(ctx, func(ctx interface{}) (types.Panel, error) { + return handler(ctx.(Context)) + }) + } +} + +// SetApp implements the method Adapter.SetApp. +func (nh *NetHTTP) SetApp(app interface{}) error { + var ( + eng *http.ServeMux + ok bool + ) + if eng, ok = app.(*http.ServeMux); !ok { + return errors.New("net/http adapter SetApp: wrong parameter") + } + nh.app = eng + return nil +} + +// AddHandler implements the method Adapter.AddHandler. +func (nh *NetHTTP) AddHandler(method, path string, handlers context.Handlers) { + url := path + reg1 := regexp.MustCompile(":(.*?)/") + reg2 := regexp.MustCompile(":(.*?)$") + url = reg1.ReplaceAllString(url, "{$1}/") + url = reg2.ReplaceAllString(url, "{$1}") + + if len(url) > 1 && url[0] == '/' && url[1] == '/' { + url = url[1:] + } + + pattern := fmt.Sprintf("%s %s", strings.ToUpper(method), url) + + nh.app.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("%+#v\n", r.URL.RawQuery) + + if r.URL.Path[len(r.URL.Path)-1] == '/' { + r.URL.Path = r.URL.Path[:len(r.URL.Path)-1] + } + + ctx := context.NewContext(r) + + params := getPathParams(pattern, r.URL.RawPath) + + for k, v := range params { + if r.URL.RawQuery == "" { + r.URL.RawQuery += strings.ReplaceAll(k, ":", "") + "=" + v + } else { + r.URL.RawQuery += "&" + strings.ReplaceAll(k, ":", "") + "=" + v + } + } + + ctx.SetHandlers(handlers).Next() + for key, head := range ctx.Response.Header { + w.Header().Set(key, head[0]) + } + if ctx.Response.Body != nil { + buf := new(bytes.Buffer) + _, _ = buf.ReadFrom(ctx.Response.Body) + w.WriteHeader(ctx.Response.StatusCode) + _, _ = w.Write(buf.Bytes()) + } else { + w.WriteHeader(ctx.Response.StatusCode) + } + }) +} + +// HandleFun is type of route methods of chi. +type HandleFun func(pattern string, handlerFn http.HandlerFunc) + +// Context wraps the Request and Response object of Chi. +type Context struct { + Request *http.Request + Response http.ResponseWriter +} + +// SetContext implements the method Adapter.SetContext. +func (*NetHTTP) SetContext(contextInterface interface{}) adapter.WebFrameWork { + var ( + ctx Context + ok bool + ) + if ctx, ok = contextInterface.(Context); !ok { + panic("net/http adapter SetContext: wrong parameter") + } + return &NetHTTP{ctx: ctx} +} + +// Name implements the method Adapter.Name. +func (*NetHTTP) Name() string { + return "net/http" +} + +// Redirect implements the method Adapter.Redirect. +func (nh *NetHTTP) Redirect() { + http.Redirect(nh.ctx.Response, nh.ctx.Request, cfg.Url(cfg.GetLoginUrl()), http.StatusFound) +} + +// SetContentType implements the method Adapter.SetContentType. +func (nh *NetHTTP) SetContentType() { + nh.ctx.Response.Header().Set("Content-Type", nh.HTMLContentType()) +} + +// Write implements the method Adapter.Write. +func (nh *NetHTTP) Write(body []byte) { + nh.ctx.Response.WriteHeader(http.StatusOK) + _, _ = nh.ctx.Response.Write(body) +} + +// GetCookie implements the method Adapter.GetCookie. +func (nh *NetHTTP) GetCookie() (string, error) { + cookie, err := nh.ctx.Request.Cookie(nh.CookieKey()) + if err != nil { + return "", err + } + return cookie.Value, err +} + +// Lang implements the method Adapter.Lang. +func (nh *NetHTTP) Lang() string { + return nh.ctx.Request.URL.Query().Get("__ga_lang") +} + +// Path implements the method Adapter.Path. +func (nh *NetHTTP) Path() string { + return nh.ctx.Request.URL.Path +} + +// Method implements the method Adapter.Method. +func (nh *NetHTTP) Method() string { + return nh.ctx.Request.Method +} + +// FormParam implements the method Adapter.FormParam. +func (nh *NetHTTP) FormParam() url.Values { + _ = nh.ctx.Request.ParseMultipartForm(32 << 20) + return nh.ctx.Request.PostForm +} + +// IsPjax implements the method Adapter.IsPjax. +func (nh *NetHTTP) IsPjax() bool { + return nh.ctx.Request.Header.Get(constant.PjaxHeader) == "true" +} + +// Query implements the method Adapter.Query. +func (nh *NetHTTP) Query() url.Values { + return nh.ctx.Request.URL.Query() +} + +// Request implements the method Adapter.Request. +func (nh *NetHTTP) Request() *http.Request { + return nh.ctx.Request +} + +// getPathParams extracts path parameters from a URL based on a given pattern. +func getPathParams(pattern, url string) map[string]string { + params := make(map[string]string) + + // Convert pattern to regex + placeholderRegex := regexp.MustCompile(`\{(\w+)\}`) + regexPattern := "^" + placeholderRegex.ReplaceAllStringFunc(pattern, func(s string) string { + return `(?P<` + s[1:len(s)-1] + `>\w+)` + }) + `$` + + // Compile regex + regex := regexp.MustCompile(regexPattern) + + // Match the URL against the regex + match := regex.FindStringSubmatch(url) + if match == nil { + return nil + } + + // Extract named groups + for i, name := range regex.SubexpNames() { + if i != 0 && name != "" { // Ignore the whole match at index 0 + params[name] = match[i] + } + } + + return params +} diff --git a/examples/nethttp/main.go b/examples/nethttp/main.go new file mode 100644 index 000000000..d39b55347 --- /dev/null +++ b/examples/nethttp/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "log" + "net/http" + "os" + "os/signal" + "path/filepath" + "strings" + "time" + + _ "github.com/GoAdminGroup/go-admin/adapter/nethttp" + _ "github.com/GoAdminGroup/go-admin/modules/db/drivers/mysql" + + "github.com/GoAdminGroup/go-admin/engine" + "github.com/GoAdminGroup/go-admin/examples/datamodel" + "github.com/GoAdminGroup/go-admin/modules/config" + "github.com/GoAdminGroup/go-admin/modules/language" + "github.com/GoAdminGroup/go-admin/plugins/example" + "github.com/GoAdminGroup/go-admin/template" + "github.com/GoAdminGroup/go-admin/template/chartjs" + "github.com/GoAdminGroup/themes/adminlte" +) + +func main() { + r := http.NewServeMux() + + eng := engine.Default() + + cfg := config.Config{ + Env: config.EnvLocal, + Databases: config.DatabaseList{ + "default": { + Host: "127.0.0.1", + Port: "3306", + User: "root", + Pwd: "root", + Name: "godmin", + MaxIdleConns: 50, + MaxOpenConns: 150, + ConnMaxLifetime: time.Hour, + Driver: config.DriverMysql, + }, + }, + UrlPrefix: "admin", + Store: config.Store{ + Path: "./uploads", + Prefix: "uploads", + }, + Language: language.EN, + IndexUrl: "/", + Debug: true, + ColorScheme: adminlte.ColorschemeSkinBlack, + } + + template.AddComp(chartjs.NewChart()) + + // customize a plugin + + examplePlugin := example.NewExample() + + // load from golang.Plugin + // + // examplePlugin := plugins.LoadFromPlugin("../datamodel/example.so") + + // customize the login page + // example: https://github.com/GoAdminGroup/demo.go-admin.cn/blob/master/main.go#L39 + // + // template.AddComp("login", datamodel.LoginPage) + + // load config from json file + // + // eng.AddConfigFromJSON("../datamodel/config.json") + + if err := eng.AddConfig(&cfg). + AddGenerators(datamodel.Generators). + AddDisplayFilterXssJsFilter(). + // add generator, first parameter is the url prefix of table when visit. + // example: + // + // "user" => http://localhost:9033/admin/info/user + // + AddGenerator("user", datamodel.GetUserTable). + AddPlugins(examplePlugin). + Use(r); err != nil { + panic(err) + } + + workDir, _ := os.Getwd() + filesDir := filepath.Join(workDir, "uploads") + FileServer(r, "/uploads", http.Dir(filesDir)) + + // you can custom your pages like: + + eng.HTML("GET", "/admin", datamodel.GetContent) + + go func() { + _ = http.ListenAndServe(":3333", r) + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + <-quit + log.Print("closing database connection") + eng.MysqlConnection().Close() +} + +// FileServer conveniently sets up a http.FileServer handler to serve +// static files from a http.FileSystem. +func FileServer(r *http.ServeMux, path string, root http.FileSystem) { + if strings.ContainsAny(path, "{}*") { + panic("FileServer does not permit URL parameters.") + } + + fs := http.StripPrefix(path, http.FileServer(root)) + + if path != "/" && path[len(path)-1] != '/' { + r.HandleFunc("GET "+path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP) + path += "/" + } + path += "*" + + r.HandleFunc("GET "+path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fs.ServeHTTP(w, r) + })) +} diff --git a/go.mod b/go.mod index fa568aa9f..a7eb516c9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/GoAdminGroup/go-admin -go 1.21.5 +go 1.22.10 require ( github.com/360EntSecGroup-Skylar/excelize v1.4.1 diff --git a/tests/frameworks/nethttp/nethttp.go b/tests/frameworks/nethttp/nethttp.go new file mode 100644 index 000000000..1323831c0 --- /dev/null +++ b/tests/frameworks/nethttp/nethttp.go @@ -0,0 +1,79 @@ +package nethttp + +import ( + // add net/http adapter + _ "github.com/GoAdminGroup/go-admin/adapter/nethttp" + "github.com/GoAdminGroup/go-admin/modules/config" + "github.com/GoAdminGroup/go-admin/modules/language" + "github.com/GoAdminGroup/go-admin/plugins/admin/modules/table" + + // add mysql driver + _ "github.com/GoAdminGroup/go-admin/modules/db/drivers/mysql" + // add postgresql driver + _ "github.com/GoAdminGroup/go-admin/modules/db/drivers/postgres" + // add sqlite driver + _ "github.com/GoAdminGroup/go-admin/modules/db/drivers/sqlite" + // add mssql driver + _ "github.com/GoAdminGroup/go-admin/modules/db/drivers/mssql" + // add adminlte ui theme + "github.com/GoAdminGroup/themes/adminlte" + + "net/http" + "os" + + "github.com/GoAdminGroup/go-admin/engine" + "github.com/GoAdminGroup/go-admin/plugins/admin" + "github.com/GoAdminGroup/go-admin/plugins/example" + "github.com/GoAdminGroup/go-admin/template" + "github.com/GoAdminGroup/go-admin/template/chartjs" + "github.com/GoAdminGroup/go-admin/tests/tables" +) + +func internalHandler() http.Handler { + r := http.NewServeMux() + + eng := engine.Default() + + adminPlugin := admin.NewAdmin(tables.Generators) + adminPlugin.AddGenerator("user", tables.GetUserTable) + examplePlugin := example.NewExample() + template.AddComp(chartjs.NewChart()) + + if err := eng.AddConfigFromJSON(os.Args[len(os.Args)-1]). + AddPlugins(adminPlugin, examplePlugin).Use(r); err != nil { + panic(err) + } + + eng.HTML("GET", "/admin", tables.GetContent) + + return r +} + +func NewHandler(dbs config.DatabaseList, gens table.GeneratorList) http.Handler { + r := http.NewServeMux() + + eng := engine.Default() + + adminPlugin := admin.NewAdmin(gens) + template.AddComp(chartjs.NewChart()) + + if err := eng.AddConfig(&config.Config{ + Databases: dbs, + UrlPrefix: "admin", + Store: config.Store{ + Path: "./uploads", + Prefix: "uploads", + }, + Language: language.EN, + IndexUrl: "/", + Debug: true, + ColorScheme: adminlte.ColorschemeSkinBlack, + }). + AddPlugins(adminPlugin).Use(r); err != nil { + panic(err) + } + + eng.HTML("GET", "/admin", tables.GetContent) + + return r +} diff --git a/tests/frameworks/nethttp/nethttp_test.go b/tests/frameworks/nethttp/nethttp_test.go new file mode 100644 index 000000000..e841bae85 --- /dev/null +++ b/tests/frameworks/nethttp/nethttp_test.go @@ -0,0 +1,19 @@ +package nethttp + +import ( + "net/http" + "testing" + + "github.com/GoAdminGroup/go-admin/tests/common" + "github.com/gavv/httpexpect" +) + +func TestNetHTTP(t *testing.T) { + common.ExtraTest(httpexpect.WithConfig(httpexpect.Config{ + Client: &http.Client{ + Transport: httpexpect.NewBinder(internalHandler()), + Jar: httpexpect.NewJar(), + }, + Reporter: httpexpect.NewAssertReporter(t), + })) +}