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.
This commit is contained in:
Max 2026-02-15 16:34:31 +08:00
parent 069f60f1f8
commit 621290d578
7 changed files with 111 additions and 34 deletions

View file

@ -77,7 +77,6 @@ func withStaticFileServer(c *gin.Context) {
// Sui file server // Sui file server
if strings.HasSuffix(c.Request.URL.Path, ".sui") { if strings.HasSuffix(c.Request.URL.Path, ".sui") {
// Default index.sui // Default index.sui
if filepath.Base(c.Request.URL.Path) == ".sui" { if filepath.Base(c.Request.URL.Path) == ".sui" {
c.Request.URL.Path = strings.TrimSuffix(c.Request.URL.Path, ".sui") + "index.sui" c.Request.URL.Path = strings.TrimSuffix(c.Request.URL.Path, ".sui") + "index.sui"
@ -94,12 +93,17 @@ func withStaticFileServer(c *gin.Context) {
if err != nil { if err != nil {
if code == 301 || code == 302 { if code == 301 || code == 302 {
url := err.Error() url := err.Error()
// fmt.Println("Redirect to: ", url)
c.Redirect(code, url) c.Redirect(code, url)
c.Done() c.Done()
return return
} }
// Guard already sent response (e.g., OAuth writes its own 401)
if c.Writer.Written() {
c.Done()
return
}
log.Error("Sui Render Error: %s", err.Error()) log.Error("Sui Render Error: %s", err.Error())
c.AbortWithStatusJSON(code, gin.H{"code": code, "message": err.Error()}) c.AbortWithStatusJSON(code, gin.H{"code": code, "message": err.Error()})
return return

View file

@ -97,6 +97,8 @@ func guardCookieTrace(r *Request) error {
// OAuth 2.1 guard - authentication only // OAuth 2.1 guard - authentication only
// This guard validates the token and sets authorized info // This guard validates the token and sets authorized info
// ACL checks are performed separately in Run() for API calls // 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 { func guardOAuth(r *Request) error {
if r.context == nil { if r.context == nil {
return fmt.Errorf("Context is nil") return fmt.Errorf("Context is nil")
@ -108,11 +110,22 @@ func guardOAuth(r *Request) error {
c := r.context c := r.context
// Authenticate only (validates token and sets authorized info) // Check token first without writing response.
if !oauth.OAuth.Authenticate(c) { // oauth.Authenticate() writes JSON + aborts on failure, which prevents
return fmt.Errorf("Not authenticated") // 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 // Get authorized info from context
info := authorized.GetInfo(c) info := authorized.GetInfo(c)
if info != nil { if info != nil {

View file

@ -237,6 +237,13 @@ func (r *Request) MakeCache() (*core.Cache, int, error) {
guardRedirect = parts[1] 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 // Cache store
cacheStore = conf.CacheStore cacheStore = conf.CacheStore
cacheTime = conf.Cache cacheTime = conf.Cache
@ -303,8 +310,8 @@ func (r *Request) MakeCache() (*core.Cache, int, error) {
// Guard the page // Guard the page
func (r *Request) Guard(c *core.Cache) (int, error) { func (r *Request) Guard(c *core.Cache) (int, error) {
// Guard not set // Guard not set or explicitly disabled
if c.Guard == "" || r.context == nil { if c.Guard == "" || c.Guard == "-" || r.context == nil {
return 200, nil return 200, nil
} }
@ -312,32 +319,27 @@ func (r *Request) Guard(c *core.Cache) (int, error) {
if guard, has := Guards[c.Guard]; has { if guard, has := Guards[c.Guard]; has {
err := guard(r) err := guard(r)
if err != nil { if err != nil {
// Redirect the page (should refector before release) // Redirect the page (takes priority over guard's own response)
if c.GuardRedirect != "" { if c.GuardRedirect != "" {
redirect := c.GuardRedirect redirect := c.GuardRedirect
data := core.Data{}
// Here may have a security issue, should be refector, in the future. // Append error code and message as query parameters
// Copy the script pointer to the request For page backend script execution ex := exception.Err(err, 403)
r.Request.Script = c.Script msg := url.QueryEscape(ex.Message)
if c.Data != "" { if strings.Contains(redirect, "?") {
data, err = r.Request.ExecString(c.Data) redirect = fmt.Sprintf("%s&code=%d&message=%s", redirect, ex.Code, msg)
if err != nil { } else {
return 500, fmt.Errorf("data error, please re-complie the page %s", err.Error()) 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) 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 // Return the error
ex := exception.Err(err, 403) ex := exception.Err(err, 403)
return ex.Code, fmt.Errorf("%s", ex.Message) return ex.Code, fmt.Errorf("%s", ex.Message)
@ -348,6 +350,10 @@ func (r *Request) Guard(c *core.Cache) (int, error) {
// Developer custom guard // Developer custom guard
err := r.processGuard(c.Guard) err := r.processGuard(c.Guard)
if err != nil { if err != nil {
// Guard already sent response
if r.context != nil && r.context.IsAborted() {
return 403, err
}
ex := exception.Err(err, 403) ex := exception.Err(err, 403)
return ex.Code, fmt.Errorf("%s", ex.Message) return ex.Code, fmt.Errorf("%s", ex.Message)
} }

View file

@ -9,6 +9,11 @@ import (
// SUIs the loaded SUI instances // SUIs the loaded SUI instances
var SUIs = map[string]SUI{} 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 // RouteMatchers the route matchers for the SUI instance
var RouteMatchers = map[*regexp.Regexp][][]*Matcher{} var RouteMatchers = map[*regexp.Regexp][][]*Matcher{}

View file

@ -185,6 +185,7 @@ type Template struct {
GlobalData []byte `json:"-"` GlobalData []byte `json:"-"`
Scripts *TemplateScirpts `json:"scripts,omitempty"` Scripts *TemplateScirpts `json:"scripts,omitempty"`
Translator string `json:"translator,omitempty"` Translator string `json:"translator,omitempty"`
Config *PageSetting `json:"config,omitempty"` // Default page config (guard, api, etc.)
BuildScript *Script `json:"-"` // __build.backend.ts / __build.backend.js BuildScript *Script `json:"-"` // __build.backend.ts / __build.backend.js
GlobalScript *Script `json:"-"` // __global.backend.ts / __global.backend.js GlobalScript *Script `json:"-"` // __global.backend.ts / __global.backend.js
} }

View file

@ -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 // Load __document.html
documentFile := filepath.Join(agent.root, "__document.html") documentFile := filepath.Join(agent.root, "__document.html")
if agent.fs.IsFile(documentFile) { if agent.fs.IsFile(documentFile) {

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
@ -157,17 +158,52 @@ func (page *Page) GetConfig() *core.PageConfig {
if fs.IsFile(confFile) { if fs.IsFile(confFile) {
content, err := fs.ReadFile(confFile) content, err := fs.ReadFile(confFile)
if err != nil { if err != nil {
return nil return page.mergeTemplateConfig(nil)
} }
var config core.PageConfig var config core.PageConfig
if err := jsoniter.Unmarshal(content, &config); err == nil { if err := jsoniter.Unmarshal(content, &config); err == nil {
p.Config = &config 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) // 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) html, config, warnings, err := page.Page.Compile(ctx, option)
if err != nil { if err != nil {
return warnings, fmt.Errorf("Compile the page %s error: %s", page.Route, err.Error()) 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{} warnings := []string{}
ctx := core.NewBuildContext(globalCtx) ctx := core.NewBuildContext(globalCtx)
// Merge template default config before compile
page.GetConfig()
_, _, messages, err := page.Page.Compile(ctx, option) _, _, messages, err := page.Page.Compile(ctx, option)
if err != nil { if err != nil {
return warnings, err return warnings, err