From 621290d5782a795b6e92e993324b9c70a0997268 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 15 Feb 2026 16:34:31 +0800 Subject: [PATCH] Enhance guard handling and template configuration merging - Update the OAuth guard to prevent automatic response writing on failure, allowing for custom error handling. - Introduce a mechanism to register default guard redirects from template configurations, improving guard management. - Refactor the page configuration merging process to prioritize page-specific settings while allowing inheritance from templates. - Ensure guards can be explicitly disabled in page configurations, enhancing flexibility in guard application. --- service/middleware.go | 14 +++++++---- sui/api/guards.go | 19 ++++++++++++--- sui/api/request.go | 48 +++++++++++++++++++++---------------- sui/core/interfaces.go | 5 ++++ sui/core/types.go | 5 ++-- sui/storages/agent/agent.go | 6 +++++ sui/storages/agent/page.go | 48 ++++++++++++++++++++++++++++++++++--- 7 files changed, 111 insertions(+), 34 deletions(-) diff --git a/service/middleware.go b/service/middleware.go index 12771702..7d3772c1 100644 --- a/service/middleware.go +++ b/service/middleware.go @@ -77,7 +77,6 @@ func withStaticFileServer(c *gin.Context) { // Sui file server if strings.HasSuffix(c.Request.URL.Path, ".sui") { - // Default index.sui if filepath.Base(c.Request.URL.Path) == ".sui" { c.Request.URL.Path = strings.TrimSuffix(c.Request.URL.Path, ".sui") + "index.sui" @@ -92,10 +91,15 @@ func withStaticFileServer(c *gin.Context) { html, code, err := r.Render() if err != nil { - if code == 301 || code == 302 { - url := err.Error() - // fmt.Println("Redirect to: ", url) - c.Redirect(code, url) + if code == 301 || code == 302 { + url := err.Error() + c.Redirect(code, url) + c.Done() + return + } + + // Guard already sent response (e.g., OAuth writes its own 401) + if c.Writer.Written() { c.Done() return } diff --git a/sui/api/guards.go b/sui/api/guards.go index 74606341..b53ce74b 100644 --- a/sui/api/guards.go +++ b/sui/api/guards.go @@ -97,6 +97,8 @@ func guardCookieTrace(r *Request) error { // OAuth 2.1 guard - authentication only // This guard validates the token and sets authorized info // ACL checks are performed separately in Run() for API calls +// NOTE: This guard does NOT write HTTP responses on failure, so that +// the caller (Guard/apiGuard) can handle redirects or custom error responses. func guardOAuth(r *Request) error { if r.context == nil { return fmt.Errorf("Context is nil") @@ -108,11 +110,22 @@ func guardOAuth(r *Request) error { c := r.context - // Authenticate only (validates token and sets authorized info) - if !oauth.OAuth.Authenticate(c) { - return fmt.Errorf("Not authenticated") + // Check token first without writing response. + // oauth.Authenticate() writes JSON + aborts on failure, which prevents + // the caller from doing redirects. So we check the token manually first. + token := oauth.OAuth.GetAccessToken(c) + if token == "" { + return fmt.Errorf("Exception|401:Not authenticated") } + if _, err := oauth.OAuth.VerifyToken(token); err != nil { + return fmt.Errorf("Exception|401:Invalid or expired token") + } + + // Token is valid, now call Authenticate to set up the full context + // (session ID, authorized info, etc.). This will succeed since token is valid. + oauth.OAuth.Authenticate(c) + // Get authorized info from context info := authorized.GetInfo(c) if info != nil { diff --git a/sui/api/request.go b/sui/api/request.go index a49716f2..941fbd6d 100644 --- a/sui/api/request.go +++ b/sui/api/request.go @@ -237,6 +237,13 @@ func (r *Request) MakeCache() (*core.Cache, int, error) { guardRedirect = parts[1] } + // Fallback: if guard has no redirect, check template default redirect + if guardRedirect == "" && guard != "" && guard != "-" { + if defaultRedirect, has := core.DefaultGuardRedirects[guard]; has { + guardRedirect = defaultRedirect + } + } + // Cache store cacheStore = conf.CacheStore cacheTime = conf.Cache @@ -303,8 +310,8 @@ func (r *Request) MakeCache() (*core.Cache, int, error) { // Guard the page func (r *Request) Guard(c *core.Cache) (int, error) { - // Guard not set - if c.Guard == "" || r.context == nil { + // Guard not set or explicitly disabled + if c.Guard == "" || c.Guard == "-" || r.context == nil { return 200, nil } @@ -312,32 +319,27 @@ func (r *Request) Guard(c *core.Cache) (int, error) { if guard, has := Guards[c.Guard]; has { err := guard(r) if err != nil { - // Redirect the page (should refector before release) + // Redirect the page (takes priority over guard's own response) if c.GuardRedirect != "" { redirect := c.GuardRedirect - data := core.Data{} - // Here may have a security issue, should be refector, in the future. - // Copy the script pointer to the request For page backend script execution - r.Request.Script = c.Script - if c.Data != "" { - data, err = r.Request.ExecString(c.Data) - if err != nil { - return 500, fmt.Errorf("data error, please re-complie the page %s", err.Error()) - } + + // Append error code and message as query parameters + ex := exception.Err(err, 403) + msg := url.QueryEscape(ex.Message) + if strings.Contains(redirect, "?") { + redirect = fmt.Sprintf("%s&code=%d&message=%s", redirect, ex.Code, msg) + } else { + redirect = fmt.Sprintf("%s?code=%d&message=%s", redirect, ex.Code, msg) } - if c.Global != "" { - global, err := r.Request.ExecString(c.Global) - if err != nil { - return 500, fmt.Errorf("global data error, please re-complie the page %s", err.Error()) - } - data["$global"] = global - } - - redirect, _ = data.Replace(redirect) return 302, fmt.Errorf("%s", redirect) } + // Guard already sent response (e.g., OAuth writes its own 401) + if r.context != nil && r.context.IsAborted() { + return 403, err + } + // Return the error ex := exception.Err(err, 403) return ex.Code, fmt.Errorf("%s", ex.Message) @@ -348,6 +350,10 @@ func (r *Request) Guard(c *core.Cache) (int, error) { // Developer custom guard err := r.processGuard(c.Guard) if err != nil { + // Guard already sent response + if r.context != nil && r.context.IsAborted() { + return 403, err + } ex := exception.Err(err, 403) return ex.Code, fmt.Errorf("%s", ex.Message) } diff --git a/sui/core/interfaces.go b/sui/core/interfaces.go index 109274a4..fdf22edd 100644 --- a/sui/core/interfaces.go +++ b/sui/core/interfaces.go @@ -9,6 +9,11 @@ import ( // SUIs the loaded SUI instances var SUIs = map[string]SUI{} +// DefaultGuardRedirects stores default guard redirect URLs from template configs. +// Key is guard name (e.g. "oauth"), value is redirect URL (e.g. "/dashboard/auth/entry"). +// Registered by template loading (e.g. agent storage) and used by MakeCache as fallback. +var DefaultGuardRedirects = map[string]string{} + // RouteMatchers the route matchers for the SUI instance var RouteMatchers = map[*regexp.Regexp][][]*Matcher{} diff --git a/sui/core/types.go b/sui/core/types.go index 91bf556e..38545155 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -185,8 +185,9 @@ type Template struct { GlobalData []byte `json:"-"` Scripts *TemplateScirpts `json:"scripts,omitempty"` Translator string `json:"translator,omitempty"` - BuildScript *Script `json:"-"` // __build.backend.ts / __build.backend.js - GlobalScript *Script `json:"-"` // __global.backend.ts / __global.backend.js + Config *PageSetting `json:"config,omitempty"` // Default page config (guard, api, etc.) + BuildScript *Script `json:"-"` // __build.backend.ts / __build.backend.js + GlobalScript *Script `json:"-"` // __global.backend.ts / __global.backend.js } // TemplateScirpts is the struct for the template scripts diff --git a/sui/storages/agent/agent.go b/sui/storages/agent/agent.go index edd4dbbf..d5eb3c95 100644 --- a/sui/storages/agent/agent.go +++ b/sui/storages/agent/agent.go @@ -90,6 +90,12 @@ func (agent *Agent) GetTemplate(id string) (core.ITemplate, error) { } } + // Register default guard redirect from template config + if tmpl.Template.Config != nil && strings.Contains(tmpl.Template.Config.Guard, ":") { + parts := strings.SplitN(tmpl.Template.Config.Guard, ":", 2) + core.DefaultGuardRedirects[parts[0]] = parts[1] + } + // Load __document.html documentFile := filepath.Join(agent.root, "__document.html") if agent.fs.IsFile(documentFile) { diff --git a/sui/storages/agent/page.go b/sui/storages/agent/page.go index 067bba4e..f85df7c3 100644 --- a/sui/storages/agent/page.go +++ b/sui/storages/agent/page.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" jsoniter "github.com/json-iterator/go" @@ -157,17 +158,52 @@ func (page *Page) GetConfig() *core.PageConfig { if fs.IsFile(confFile) { content, err := fs.ReadFile(confFile) if err != nil { - return nil + return page.mergeTemplateConfig(nil) } var config core.PageConfig if err := jsoniter.Unmarshal(content, &config); err == nil { p.Config = &config - return p.Config + return page.mergeTemplateConfig(p.Config) } } - return nil + return page.mergeTemplateConfig(nil) +} + +// mergeTemplateConfig merges template default config into page config (page config takes priority). +// Use guard: "-" in page config to explicitly disable guard inheritance. +func (page *Page) mergeTemplateConfig(cfg *core.PageConfig) *core.PageConfig { + tmplConfig := page.tmpl.Template.Config + if tmplConfig == nil { + return cfg + } + + if cfg == nil { + cfg = &core.PageConfig{PageSetting: *tmplConfig} + page.Page.Config = cfg + return cfg + } + + // Merge guard (page config takes priority, "-" means explicitly no guard) + if cfg.Guard == "" { + // Page has no guard, use template's guard (with redirect) + cfg.Guard = tmplConfig.Guard + } else if !strings.Contains(cfg.Guard, ":") && strings.Contains(tmplConfig.Guard, ":") { + // Page has guard without redirect (e.g. "oauth"), template has redirect (e.g. "oauth:/login") + // Inherit redirect from template if same guard type + tmplParts := strings.SplitN(tmplConfig.Guard, ":", 2) + if tmplParts[0] == cfg.Guard { + cfg.Guard = tmplConfig.Guard + } + } + + // Merge API guard config + if cfg.API == nil && tmplConfig.API != nil { + cfg.API = tmplConfig.API + } + + return cfg } // SaveTemp save the page temporarily (not supported for agent pages) @@ -293,6 +329,9 @@ func (page *Page) Build(globalCtx *core.GlobalBuildContext, option *core.BuildOp } } + // Merge template default config before compile (page config takes priority) + page.GetConfig() + html, config, warnings, err := page.Page.Compile(ctx, option) if err != nil { return warnings, fmt.Errorf("Compile the page %s error: %s", page.Route, err.Error()) @@ -444,6 +483,9 @@ func (page *Page) Trans(globalCtx *core.GlobalBuildContext, option *core.BuildOp warnings := []string{} ctx := core.NewBuildContext(globalCtx) + // Merge template default config before compile + page.GetConfig() + _, _, messages, err := page.Page.Compile(ctx, option) if err != nil { return warnings, err