diff --git a/service/guard.go b/service/guard.go index 63a3d16a..1c1d23df 100644 --- a/service/guard.go +++ b/service/guard.go @@ -6,13 +6,16 @@ import ( "github.com/gin-gonic/gin" "github.com/yaoapp/yao/helper" "github.com/yaoapp/yao/table" + + widget_table "github.com/yaoapp/yao/widgets/table" ) // Guards 服务中间件 var Guards = map[string]gin.HandlerFunc{ - "bearer-jwt": bearerJWT, // JWT 鉴权 - "cross-domain": crossDomain, // 跨域许可 - "table-guard": table.Guard, // Table Guard + "bearer-jwt": bearerJWT, // JWT 鉴权 + "cross-domain": crossDomain, // 跨域许可 + "table-guard": table.Guard, // Table Guard + "widget-table": widget_table.Guard, // Widget Table Guard } // JWT 鉴权 @@ -20,7 +23,7 @@ func bearerJWT(c *gin.Context) { tokenString := c.Request.Header.Get("Authorization") tokenString = strings.TrimSpace(strings.TrimPrefix(tokenString, "Bearer ")) if tokenString == "" { - c.JSON(403, gin.H{"code": 403, "message": "无权访问该页面"}) + c.JSON(403, gin.H{"code": 403, "message": "No permission"}) c.Abort() return } diff --git a/widgets/action/guard.go b/widgets/action/guard.go new file mode 100644 index 00000000..965ce060 --- /dev/null +++ b/widgets/action/guard.go @@ -0,0 +1,30 @@ +package action + +import ( + "strings" + + "github.com/gin-gonic/gin" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/log" +) + +// UseGuard using the guard in action +func (p *Process) UseGuard(c *gin.Context, id string) error { + guards := strings.Split(p.Guard, ",") + for _, guard := range guards { + guard = strings.TrimSpace(guard) + log.Trace("Widget: %s Guard: %s", id, guard) + if guard == "-" { + return nil + } + + if guard != "" { + if middleware, has := gou.HTTPGuards[guard]; has { + middleware(c) + continue + } + gou.ProcessGuard(guard)(c) + } + } + return nil +} diff --git a/widgets/table/action.go b/widgets/table/action.go index 25ec3676..0c3f983e 100644 --- a/widgets/table/action.go +++ b/widgets/table/action.go @@ -14,59 +14,73 @@ var processActionDefaults = map[string]*action.Process{ "Setting": { Name: "yao.table.Setting", + Guard: "bearer-jwt", Process: "yao.table.Xgen", Default: []interface{}{nil}, }, "Component": { Name: "yao.table.Component", + Guard: "bearer-jwt", Default: []interface{}{nil, nil, nil}, }, "Search": { Name: "yao.table.Search", + Guard: "bearer-jwt", Default: []interface{}{nil, 1, 20}, }, "Get": { Name: "yao.table.Get", + Guard: "bearer-jwt", Default: []interface{}{nil}, }, "Find": { Name: "yao.table.Find", + Guard: "bearer-jwt", Default: []interface{}{nil, nil}, }, "Save": { Name: "yao.table.Save", + Guard: "bearer-jwt", Default: []interface{}{nil}, }, "Create": { Name: "yao.table.Create", + Guard: "bearer-jwt", Default: []interface{}{nil}, }, "Insert": { Name: "yao.table.Insert", + Guard: "bearer-jwt", Default: []interface{}{nil, nil}, }, "Update": { Name: "yao.table.Update", + Guard: "bearer-jwt", Default: []interface{}{nil, nil}, }, "UpdateWhere": { Name: "yao.table.UpdateWhere", + Guard: "bearer-jwt", Default: []interface{}{nil, nil}, }, "UpdateIn": { Name: "yao.table.UpdateIn", + Guard: "bearer-jwt", Default: []interface{}{nil, nil}, }, "Delete": { Name: "yao.table.Delete", + Guard: "bearer-jwt", Default: []interface{}{nil}, }, "DeleteWhere": { Name: "yao.table.DeleteWhere", + Guard: "bearer-jwt", Default: []interface{}{nil}, }, "DeleteIn": { Name: "yao.table.DeleteIn", + Guard: "bearer-jwt", Default: []interface{}{nil}, }, } diff --git a/widgets/table/api.go b/widgets/table/api.go index e51739d2..d22f87c0 100644 --- a/widgets/table/api.go +++ b/widgets/table/api.go @@ -1,11 +1,85 @@ package table import ( + "fmt" + + "github.com/gin-gonic/gin" jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou" "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/widgets/action" ) +// Guard table widget guard +func Guard(c *gin.Context) { + + id := c.Param("id") + if id == "" { + abort(c, 400, "the table widget id does not found") + return + } + + tab, has := Tables[id] + if !has { + abort(c, 404, fmt.Sprintf("the table widget %s does not exist", id)) + return + } + + act, err := tab.getAction(c.FullPath()) + if err != nil { + abort(c, 404, err.Error()) + return + } + + err = act.UseGuard(c, id) + if err != nil { + abort(c, 400, err.Error()) + return + } + +} + +func abort(c *gin.Context, code int, message string) { + c.JSON(code, gin.H{"code": code, "message": message}) + c.Abort() +} + +func (table *DSL) getAction(path string) (*action.Process, error) { + + switch path { + case "/api/__yao/table/:id/setting": + return table.Action.Setting, nil + case "/api/__yao/table/:id/component/:xpath/:method": + return table.Action.Component, nil + case "/api/__yao/table/:id/search": + return table.Action.Search, nil + case "/api/__yao/table/:id/get": + return table.Action.Get, nil + case "/api/__yao/table/:id/find/:primary": + return table.Action.Find, nil + case "/api/__yao/table/:id/save": + return table.Action.Save, nil + case "/api/__yao/table/:id/create": + return table.Action.Create, nil + case "/api/__yao/table/:id/insert": + return table.Action.Insert, nil + case "/api/__yao/table/:id/update/:primary": + return table.Action.Update, nil + case "/api/__yao/table/:id/update/in": + return table.Action.UpdateIn, nil + case "/api/__yao/table/:id/update/where": + return table.Action.UpdateWhere, nil + case "/api/__yao/table/:id/delete/:primary": + return table.Action.Delete, nil + case "/api/__yao/table/:id/delete/in": + return table.Action.DeleteIn, nil + case "/api/__yao/table/:id/delete/where": + return table.Action.DeleteWhere, nil + } + + return nil, fmt.Errorf("the table widget %s %s action does not exist", table.ID, path) +} + // export API func exportAPI() error { @@ -13,7 +87,7 @@ func exportAPI() error { Name: "Widget Table API", Description: "Widget Table API", Version: share.VERSION, - Guard: "-", + Guard: "widget-table", Group: "__yao/table", Paths: []gou.Path{}, } diff --git a/widgets/table/api_test.go b/widgets/table/api_test.go new file mode 100644 index 00000000..9fd44707 --- /dev/null +++ b/widgets/table/api_test.go @@ -0,0 +1,139 @@ +package table + +import ( + "fmt" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/yaoapp/kun/any" + "github.com/yaoapp/yao/network" + "github.com/yaoapp/yao/widgets/test" +) + +var guards = map[string]gin.HandlerFunc{ + "bearer-jwt": test.GuardBearerJWT, + "widget-table": Guard, +} + +func TestAPISetting(t *testing.T) { + port := start(t) + defer test.Stop(func() {}) + + req := test.NewRequest(port).Route("/api/__yao/table/pet/setting") + res, err := req.Get() + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 403, res.Status()) + + req = test.NewRequest(port).Route("/api/__yao/table/pet/setting").Token(token(t)) + res, err = req.Get() + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 200, res.Status()) + + v, err := res.Map() + if err != nil { + t.Fatal(err) + } + data := any.Of(v).MapStr().Dot() + assert.Equal(t, "/api/xiang/import/pet", data.Get("header.preset.import.api.import")) + assert.Equal(t, "跳转", data.Get("header.preset.import.operation.0.title")) + assert.Equal(t, "/api/__yao/table/pet/component/fields.table.入院状态.view.props.xProps/remote", data.Get("fields.table.入院状态.view.props.xProps.remote.api")) + assert.Equal(t, "/api/__yao/table/pet/component/fields.table.入院状态.edit.props.xProps/remote", data.Get("fields.table.入院状态.edit.props.xProps.remote.api")) +} + +func TestAPISave(t *testing.T) { + port := start(t) + defer test.Stop(func() {}) + + payload := map[string]interface{}{ + "name": "New Pet", + "type": "cat", + "status": "checked", + "mode": "enabled", + "stay": 66, + "cost": 24, + "doctor_id": 1, + } + + req := test.NewRequest(port).Route("/api/__yao/table/pet/save").Data(payload) + res, err := req.Post() + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 403, res.Status()) + + req = test.NewRequest(port).Route("/api/__yao/table/pet/save").Data(payload).Token(token(t)) + res, err = req.Post() + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 200, res.Status()) + + v, err := res.Int() + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, 4, v) +} + +func TestAPICustomGuard(t *testing.T) { + + port := start(t) + defer test.Stop(func() {}) + + req := test.NewRequest(port).Route("/api/__yao/table/pet/find/1") + res, err := req.Get() + if err != nil { + t.Fatal(err) + } + + req = test.NewRequest(port).Route("/api/__yao/table/pet/get") + res, err = req.Get() + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 403, res.Status()) + + req = test.NewRequest(port).Route("/api/__yao/table/pet/get").Token(token(t)).Header("Unit-Test", "yes") + res, err = req.Get() + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 418, res.Status()) + + req = test.NewRequest(port).Route("/api/__yao/table/pet/get").Token(token(t)) + res, err = req.Get() + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 200, res.Status()) +} + +func start(t *testing.T) int { + port := network.FreePort() + load(t) + clear(t) + testData(t) + go test.Start(t, guards, port) + time.Sleep(200 * time.Millisecond) + return port +} + +func token(t *testing.T) string { + res, err := test.AutoLogin(1) + if err != nil { + t.Fatal(err) + } + + token, ok := res["token"].(string) + if !ok { + t.Fatal(fmt.Errorf("get token error %v", res)) + } + return token +} diff --git a/widgets/table/table_test.go b/widgets/table/table_test.go index 09573e74..76461d58 100644 --- a/widgets/table/table_test.go +++ b/widgets/table/table_test.go @@ -1,15 +1,15 @@ package table import ( - "os" "testing" "github.com/stretchr/testify/assert" "github.com/yaoapp/yao/config" - "github.com/yaoapp/yao/lang" + "github.com/yaoapp/yao/flow" "github.com/yaoapp/yao/model" "github.com/yaoapp/yao/script" - "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/widgets/app" + "github.com/yaoapp/yao/widgets/test" ) func TestLoad(t *testing.T) { @@ -24,18 +24,13 @@ func TestLoad(t *testing.T) { func prepare(t *testing.T, language ...string) { - // langs - if len(language) < 1 { - os.Unsetenv("YAO_LANG") - } else { - os.Setenv("YAO_LANG", language[0]) + err := test.LoadEngine(language...) + if err != nil { + t.Fatal(err) } - lang.Load(config.Conf) - - share.DBConnect(config.Conf.DB) // removed later // load scripts - err := script.Load(config.Conf) + err = script.Load(config.Conf) if err != nil { t.Fatal(err) } @@ -46,7 +41,17 @@ func prepare(t *testing.T, language ...string) { t.Fatal(err) } - // load scripts + // load flows + err = flow.Load(config.Conf) + if err != nil { + t.Fatal(err) + } + + // load app widget + err = app.LoadAndExport(config.Conf) + if err != nil { + t.Fatal(err) + } // export err = Export() diff --git a/widgets/test/engine.go b/widgets/test/engine.go new file mode 100644 index 00000000..7844b8dc --- /dev/null +++ b/widgets/test/engine.go @@ -0,0 +1,39 @@ +package test + +import ( + "os" + "path/filepath" + + "github.com/yaoapp/gou" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/lang" + "github.com/yaoapp/yao/model" + "github.com/yaoapp/yao/share" +) + +// LoadEngine load engine +func LoadEngine(language ...string) error { + + // langs + if len(language) < 1 { + os.Unsetenv("YAO_LANG") + } else { + os.Setenv("YAO_LANG", language[0]) + } + lang.Load(config.Conf) + + share.DBConnect(config.Conf.DB) // removed later + gou.LoadCrypt(`{}`, "PASSWORD") + gou.LoadCrypt(`{}`, "AES") + + // load engine models + dev := os.Getenv("YAO_DEV") + if dev != "" { + err := model.LoadFrom(filepath.Join(dev, "yao", "models"), "xiang.") + if err != nil { + return err + } + } + + return nil +} diff --git a/widgets/test/request.go b/widgets/test/request.go new file mode 100644 index 00000000..854cf809 --- /dev/null +++ b/widgets/test/request.go @@ -0,0 +1,224 @@ +package test + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "time" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" + "github.com/yaoapp/gou/session" + "github.com/yaoapp/yao/helper" +) + +// Request request +type Request struct { + host string + port int + route string + method string + data map[string]interface{} + params map[string]string + headers map[string]string +} + +// Response response +type Response struct { + status int + body []byte +} + +// NewRequest create a new request +func NewRequest(port int) *Request { + return &Request{ + host: "127.0.0.1", + port: port, + data: map[string]interface{}{}, + params: map[string]string{}, + headers: map[string]string{}, + } +} + +// Token set token +func (r *Request) Token(token string) *Request { + r.headers["Authorization"] = fmt.Sprintf("Bearer %s", token) + return r +} + +// Header set header +func (r *Request) Header(key string, value string) *Request { + r.headers[key] = value + return r +} + +// Param set saram +func (r *Request) Param(key string, value string) *Request { + r.params[key] = value + return r +} + +// Data set data +func (r *Request) Data(data map[string]interface{}) *Request { + r.data = data + return r +} + +// Route set the route +func (r *Request) Route(route string) *Request { + r.route = route + return r +} + +// Get request +func (r *Request) Get() (*Response, error) { + r.method = "GET" + return r.Send() +} + +// Post request +func (r *Request) Post() (*Response, error) { + r.method = "POST" + return r.Send() +} + +// Send request +func (r *Request) Send() (*Response, error) { + + client := http.Client{} + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // set body + var data io.Reader = nil + if len(r.data) > 0 { + content, err := jsoniter.Marshal(r.data) + if err != nil { + return nil, err + } + data = bytes.NewBuffer(content) + } + + url := fmt.Sprintf("http://%s:%d%s", r.host, r.port, r.route) + req, err := http.NewRequestWithContext(ctx, r.method, url, data) + if err != nil { + return nil, err + } + + // Set header + for key, value := range r.headers { + req.Header.Add(key, value) + } + + if _, has := r.headers["Content-Type"]; !has { + req.Header.Add("Content-Type", "application/json") + } + + // Set Parms + if len(r.params) > 0 { + q := req.URL.Query() + for key, value := range r.params { + q.Add(key, value) + } + req.URL.RawQuery = q.Encode() + } + + // Send Request + res, err := client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + // response body + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + p := &Response{ + status: res.StatusCode, + body: body, + } + + return p, nil +} + +// Map to map +func (p *Response) Map() (map[string]interface{}, error) { + v := map[string]interface{}{} + err := jsoniter.Unmarshal(p.body, &v) + if err != nil { + return nil, err + } + return v, nil +} + +// Int to int +func (p *Response) Int() (int, error) { + v := 0 + err := jsoniter.Unmarshal(p.body, &v) + if err != nil { + return 0, err + } + return v, nil +} + +// Status get the reaponse status +func (p *Response) Status() int { + return p.status +} + +// Body get the reaponse body +func (p *Response) Body() string { + return string(p.body) +} + +// To cast to custom sturct +func (p *Response) To(v interface{}) error { + err := jsoniter.Unmarshal(p.body, v) + if err != nil { + return err + } + return nil +} + +// AutoLogin auto login +func AutoLogin(id int) (map[string]interface{}, error) { + + user := gou.Select("xiang.user") + row, err := user.Find(id, gou.QueryParam{Select: []interface{}{"id", "password", "name", "type", "email", "mobile", "extra"}}) + if err != nil { + return nil, err + } + expiresAt := time.Now().Unix() + 3600 + sid := session.ID() + token := helper.JwtMake(id, map[string]interface{}{}, map[string]interface{}{ + "expires_at": expiresAt, + "sid": sid, + "issuer": "xiang", + }) + session.Global().Expire(time.Duration(token.ExpiresAt)*time.Second).ID(sid).Set("user_id", id) + session.Global().ID(sid).Set("user", row) + session.Global().ID(sid).Set("issuer", "xiang") + + p, err := gou.ProcessOf("yao.app.menu") + if err != nil { + return nil, err + } + + menus, err := p.Exec() + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "expires_at": token.ExpiresAt, + "token": token.Token, + "user": row, + "menus": menus, + }, nil +} diff --git a/widgets/test/server.go b/widgets/test/server.go new file mode 100644 index 00000000..e517ec8f --- /dev/null +++ b/widgets/test/server.go @@ -0,0 +1,65 @@ +package test + +import ( + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/yaoapp/gou" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/helper" + "github.com/yaoapp/yao/share" +) + +var shutdown = make(chan bool, 1) +var shutdownComplete = make(chan bool, 1) + +// Start the api server +func Start(t *testing.T, guards map[string]gin.HandlerFunc, port int) error { + + err := share.SessionStart() + if err != nil { + return err + } + + gin.SetMode(gin.ReleaseMode) + gou.SetHTTPGuards(guards) + gou.ServeHTTP( + gou.Server{ + Host: "127.0.0.1", + Port: port, + Root: "/api", + Allows: config.Conf.AllowFrom, + }, + shutdown, func(s gou.Server) { + shutdownComplete <- true + }, + ) + + return nil +} + +// Stop the api server +func Stop(onComplete func()) { + shutdown <- true + select { + case <-shutdownComplete: + share.SessionStop() + onComplete() + } +} + +// GuardBearerJWT test guard +func GuardBearerJWT(c *gin.Context) { + tokenString := c.Request.Header.Get("Authorization") + tokenString = strings.TrimSpace(strings.TrimPrefix(tokenString, "Bearer ")) + + if tokenString == "" { + c.JSON(403, gin.H{"code": 403, "message": "No permission"}) + c.Abort() + return + } + + claims := helper.JwtValidate(tokenString) + c.Set("__sid", claims.SID) +}