diff --git a/service/middleware.go b/service/middleware.go index 77b7d9d8..977122db 100644 --- a/service/middleware.go +++ b/service/middleware.go @@ -1,6 +1,7 @@ package service import ( + "fmt" "path/filepath" "strings" @@ -81,6 +82,13 @@ func withStaticFileServer(c *gin.Context) { html, code, err := r.Render() if err != nil { + if code == 301 || code == 302 { + fmt.Println(err.Error()) + c.Redirect(code, err.Error()) + c.Done() + return + } + log.Error("Sui Render Error: %s", err.Error()) c.AbortWithError(code, err) return diff --git a/sui/api/guards.go b/sui/api/guards.go new file mode 100644 index 00000000..8dbe93fe --- /dev/null +++ b/sui/api/guards.go @@ -0,0 +1,141 @@ +package api + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/gin-gonic/gin" + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou/process" + "github.com/yaoapp/yao/helper" +) + +// Guards middlewares +var Guards = map[string]func(c *gin.Context) error{ + "bearer-jwt": guardBearerJWT, // Bearer JWT + "query-jwt": guardQueryJWT, // Get JWT Token from query string "__tk" + "cookie-jwt": guardCookieJWT, // Get JWT Token from cookie "__tk" + +} + +// JWT Bearer JWT +func guardBearerJWT(c *gin.Context) error { + + 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 fmt.Errorf("No permission") + } + + claims := helper.JwtValidate(tokenString) + c.Set("__sid", claims.SID) + return nil +} + +// JWT Bearer JWT +func guardCookieJWT(c *gin.Context) error { + tokenString, err := c.Cookie("__tk") + if err != nil { + c.JSON(403, gin.H{"code": 403, "message": "No permission"}) + c.Abort() + return fmt.Errorf("No permission") + } + + if tokenString == "" { + c.JSON(403, gin.H{"code": 403, "message": "No permission"}) + c.Abort() + return fmt.Errorf("No permission") + } + + claims := helper.JwtValidate(tokenString) + c.Set("__sid", claims.SID) + return nil +} + +// JWT Bearer JWT +func guardQueryJWT(c *gin.Context) error { + tokenString := c.Query("__tk") + if tokenString == "" { + c.JSON(403, gin.H{"code": 403, "message": "No permission"}) + c.Abort() + return fmt.Errorf("No permission") + } + + claims := helper.JwtValidate(tokenString) + c.Set("__sid", claims.SID) + return nil +} + +// ProcessGuard guard process +func (r *Request) processGuard(name string) error { + var body interface{} + c := r.context + + if c.Request.Body != nil { + + bodyBytes, err := io.ReadAll(c.Request.Body) + if err == nil { + if strings.HasPrefix(strings.ToLower(c.Request.Header.Get("Content-Type")), "application/json") { + jsoniter.Unmarshal(bodyBytes, &body) + } else { + body = string(bodyBytes) + } + } + + // Reset body + c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } + + params := map[string]string{} + for _, param := range c.Params { + params[param.Key] = param.Value + } + + args := []interface{}{ + r.URL, // page url + r.Params, // page params + r.Query, // query string + r.Payload, // payload + r.Headers, // Request headers + } + + process, err := process.Of(name, args...) + if err != nil { + c.JSON(403, gin.H{"code": 403, "message": fmt.Sprintf("Guard: %s %s", name, err.Error())}) + c.Abort() + return err + } + + if sid, has := c.Get("__sid"); has { // 设定会话ID + if sid, ok := sid.(string); ok { + process.WithSID(sid) + } + } + + if global, has := c.Get("__global"); has { // 设定全局变量 + if global, ok := global.(map[string]interface{}); ok { + process.WithGlobal(global) + } + } + + v, err := process.Exec() + if err != nil { + return err + } + + if data, ok := v.(map[string]interface{}); ok { + if sid, ok := data["__sid"].(string); ok { + c.Set("__sid", sid) + } + + if global, ok := data["__global"].(map[string]interface{}); ok { + c.Set("__global", global) + } + } + + return nil +} diff --git a/sui/api/request.go b/sui/api/request.go index 5e6fdf5b..316eaf10 100644 --- a/sui/api/request.go +++ b/sui/api/request.go @@ -8,7 +8,9 @@ import ( "strings" "github.com/gin-gonic/gin" + jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou/application" + "github.com/yaoapp/kun/exception" "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/sui/core" ) @@ -17,6 +19,7 @@ import ( type Request struct { File string *core.Request + context *gin.Context } // NewRequestContext is the constructor for Request. @@ -45,7 +48,8 @@ func NewRequestContext(c *gin.Context) (*Request, int, error) { path := strings.TrimSuffix(c.Request.URL.Path, ".sui") return &Request{ - File: file, + File: file, + context: c, Request: &core.Request{ Method: c.Request.Method, Query: c.Request.URL.Query(), @@ -82,6 +86,21 @@ func (r *Request) Render() (string, int, error) { return "", 500, err } + guard := "" + configText := "" + configSel := doc.Find("script[name=config]") + if configSel != nil && configSel.Length() > 0 { + configText = configSel.Text() + configSel.Remove() + + var conf core.PageConfig + err := jsoniter.UnmarshalFromString(configText, &conf) + if err != nil { + return "", 500, fmt.Errorf("config error, please re-complie the page %s", err.Error()) + } + guard = conf.Guard + } + dataText := "" dataSel := doc.Find("script[name=data]") if dataSel != nil && dataSel.Length() > 0 { @@ -107,10 +126,31 @@ func (r *Request) Render() (string, int, error) { Data: dataText, Global: globalDataText, HTML: html, + Guard: guard, + Config: configText, } log.Trace("The page %s is cached", r.File) } + // Guard the page + if c.Guard != "" && r.context != nil { + + if guard, has := Guards[c.Guard]; has { + err := guard(r.context) + if err != nil { + ex := exception.Err(err, 403) + return "", ex.Code, fmt.Errorf("%s", ex.Message) + } + } else { + // Process the guard + err := r.processGuard(c.Guard) + if err != nil { + ex := exception.Err(err, 403) + return "", ex.Code, fmt.Errorf("%s", ex.Message) + } + } + } + var err error data := core.Data{} if c.Data != "" { @@ -128,6 +168,12 @@ func (r *Request) Render() (string, int, error) { data["$global"] = global } + // Set the page request data + data["$payload"] = r.Request.Payload + data["$query"] = r.Request.Query + data["$param"] = r.Request.Params + data["$url"] = r.Request.URL + printData := false if r.Query != nil && r.Query.Has("__sui_print_data") { printData = true diff --git a/sui/core/build.go b/sui/core/build.go index e92bade0..340cae3f 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -3,6 +3,7 @@ package core import ( "bufio" "fmt" + "path/filepath" "regexp" "strings" @@ -43,11 +44,17 @@ func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error doc.Selection.Find("head").AppendHtml(style) // Add Script - script, err := page.BuildScript(option) + code, scripts, err := page.BuildScript(option) if err != nil { warnings = append(warnings, err.Error()) } - doc.Selection.Find("body").AppendHtml(script) + if scripts != nil { + for _, script := range scripts { + doc.Selection.Find("body").AppendHtml("\n" + `` + "\n") + } + } + doc.Selection.Find("body").AppendHtml(code) + return doc, warnings, nil } @@ -89,7 +96,7 @@ func (page *Page) BuildForImport(option *BuildOption, slots map[string]interface warnings = append(warnings, err.Error()) } - script, err := page.BuildScript(option) + code, _, err := page.BuildScript(option) if err != nil { warnings = append(warnings, err.Error()) } @@ -108,7 +115,7 @@ func (page *Page) BuildForImport(option *BuildOption, slots map[string]interface // Replace the slots html, _ = Data(data).ReplaceUse(slotRe, html) - return html, style, script, warnings, nil + return html, style, code, warnings, nil } func (page *Page) parse(doc *goquery.Document, option *BuildOption, warnings []string) error { @@ -267,8 +274,12 @@ func (page *Page) BuildStyle(option *BuildOption) (string, error) { } code := page.Codes.CSS.Code + + // Replace the assets if !option.IgnoreAssetRoot { - code = strings.ReplaceAll(page.Codes.CSS.Code, "@assets", option.AssetRoot) + code = AssetsRe.ReplaceAllStringFunc(code, func(match string) string { + return strings.ReplaceAll(match, "@assets", option.AssetRoot) + }) } if option.Namespace != "" { @@ -282,43 +293,65 @@ func (page *Page) BuildStyle(option *BuildOption) (string, error) { return "", err } - return fmt.Sprintf("\n", res), nil + return fmt.Sprintf("\n", res), nil } // BuildScript build the script -func (page *Page) BuildScript(option *BuildOption) (string, error) { +func (page *Page) BuildScript(option *BuildOption) (string, []string, error) { if page.Codes.JS.Code == "" && page.Codes.TS.Code == "" { - return "", nil + return "", nil, nil } if page.Codes.TS.Code != "" { - res, err := page.CompileTS([]byte(page.Codes.TS.Code), false) + code, scripts, err := page.CompileTS([]byte(page.Codes.TS.Code), false) if err != nil { - return "", err + return "", nil, err + } + + // Replace the assets + if !option.IgnoreAssetRoot { + code = AssetsRe.ReplaceAllFunc(code, func(match []byte) []byte { + return []byte(strings.ReplaceAll(string(match), "@assets", option.AssetRoot)) + }) + + if scripts != nil { + for i, script := range scripts { + scripts[i] = filepath.Join(option.AssetRoot, script) + } + } } if option.Namespace == "" { - return fmt.Sprintf("\n", res), nil + return fmt.Sprintf("\n", code), scripts, nil } - return fmt.Sprintf("\n", option.Namespace, addTabToEachLine(string(res))), nil + return fmt.Sprintf("\n", option.Namespace, addTabToEachLine(string(code))), scripts, nil } - code := page.Codes.JS.Code - if !option.IgnoreAssetRoot { - code = strings.ReplaceAll(page.Codes.JS.Code, "@assets", option.AssetRoot) - } - - res, err := page.CompileJS([]byte(code), false) + code, scripts, err := page.CompileJS([]byte(page.Codes.JS.Code), false) if err != nil { - return "", err + return "", nil, err + } + + // Replace the assets + if !option.IgnoreAssetRoot { + code = AssetsRe.ReplaceAllFunc(code, func(match []byte) []byte { + return []byte(strings.ReplaceAll(string(match), "@assets", option.AssetRoot)) + }) + + if scripts != nil { + for i, script := range scripts { + scripts[i] = filepath.Join(option.AssetRoot, script) + } + } } if option.Namespace == "" { - return fmt.Sprintf("\n", res), nil + return fmt.Sprintf("\n", code), scripts, nil } - return fmt.Sprintf("\n", option.Namespace, addTabToEachLine(string(res))), nil + + return fmt.Sprintf("\n", option.Namespace, addTabToEachLine(string(code))), scripts, nil } func addTabToEachLine(input string, prefix ...string) string { diff --git a/sui/core/compile.go b/sui/core/compile.go index d5dd7718..35da3ad3 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -8,6 +8,13 @@ import ( "github.com/yaoapp/kun/log" ) +var quoteRe = "'\"`" +var importRe = regexp.MustCompile(`import\s*\t*\n*[^;]*;`) // import { foo, bar } from 'hello'; ... +var importAssetsRe = regexp.MustCompile(`import\s*\t*\n*\s*['"]@assets\/([^'"]+)['"];`) // import '@assets/foo.js'; or import "@assets/foo.js"; + +// AssetsRe is the regexp for assets +var AssetsRe = regexp.MustCompile(`[` + quoteRe + `]@assets\/([^` + quoteRe + `]+)[` + quoteRe + `]`) // '@assets/foo.js' or "@assets/foo.js" or `@assets/foo` + // Compile the page func (page *Page) Compile(option *BuildOption) (string, error) { @@ -22,6 +29,17 @@ func (page *Page) Compile(option *BuildOption) (string, error) { } } + // Page Config + page.Config = page.GetConfig() + + // Config Data + if page.Config != nil { + doc.Find("body").AppendHtml("\n\n" + `\n\n", + ) + } + // Page Data if page.Codes.DATA.Code != "" { doc.Find("body").AppendHtml("\n\n" + `