From 684665f0be92a96a742a49867664a0730ed77f8c Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Thu, 17 Sep 2026 06:16:57 +0800 Subject: [PATCH] feat: assign per-user WebDAV paths --- admin_users.go | 20 +++++++++++++++++--- main.go | 1 + user_webdav.go | 42 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/admin_users.go b/admin_users.go index 7ef1ca9..967fdb9 100644 --- a/admin_users.go +++ b/admin_users.go @@ -3,6 +3,7 @@ package main import ( "errors" "fmt" + "regexp" "strconv" "strings" @@ -14,6 +15,8 @@ func (a *App) audit(operator uint, action, targetType, targetID, metadata string _ = a.db.Create(&AuditLog{OperatorID: operator, Action: action, TargetType: targetType, TargetID: targetID, Metadata: metadata}).Error } +var usernamePattern = regexp.MustCompile(`^[A-Za-z]{4,10}$`) + func defaultPermissions(role string) []string { if role == RolePrivileged { return []string{PermDownload, PermRedeem, PermWebDAV, PermWebDAVConfig} @@ -59,8 +62,13 @@ func (a *App) adminCreateUser(c *gin.Context) { Role string `json:"role"` Credits int64 `json:"credits"` } - if c.BindJSON(&in) != nil || strings.TrimSpace(in.Username) == "" || len(in.Password) < 8 { - c.JSON(400, gin.H{"error": "username and password(8+) required"}) + if c.BindJSON(&in) != nil { + c.JSON(400, gin.H{"error": "invalid request body"}) + return + } + in.Username = strings.TrimSpace(in.Username) + if !usernamePattern.MatchString(in.Username) || len(in.Password) < 8 { + c.JSON(400, gin.H{"error": "username must be 4-10 English letters and password must be 8+ characters"}) return } if in.Role == "" { @@ -78,10 +86,16 @@ func (a *App) adminCreateUser(c *gin.Context) { admin := c.MustGet("user").(*User) var u User err = a.db.Transaction(func(tx *gorm.DB) error { - u = User{Username: strings.TrimSpace(in.Username), PasswordHash: hash, Role: in.Role, Status: UserActive} + u = User{Username: in.Username, PasswordHash: hash, Role: in.Role, Status: UserActive} if err := tx.Create(&u).Error; err != nil { return err } + if in.Role != RoleAdmin { + root := cleanWebDAVRoot(strings.TrimRight(a.cfg.WebDAVPath, "/") + "/" + u.Username) + if err := tx.Create(&WebDAVConfig{UserID: u.ID, Enabled: true, RootPath: root}).Error; err != nil { + return err + } + } if err := tx.Create(&CreditAccount{UserID: u.ID, Balance: in.Credits}).Error; err != nil { return err } diff --git a/main.go b/main.go index 298cdec..79060b2 100644 --- a/main.go +++ b/main.go @@ -22,6 +22,7 @@ func main() { a := &App{db: db, cfg: cfg, queue: make(chan uint, 100), httpClient: client} a.loadSettings() a.bootstrapAdmin() + must(a.initializeUserWebDAVDefaults()) must(backfillMediaAssets(db, a.cfg.DownloadDir)) if os.Getenv("MIGRATION_CHECK") == "1" { var users, tasks, assets int64 diff --git a/user_webdav.go b/user_webdav.go index 575c846..51e0549 100644 --- a/user_webdav.go +++ b/user_webdav.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/studio-b12/gowebdav" + "gorm.io/gorm" ) func cleanWebDAVRoot(root string) string { @@ -18,6 +19,28 @@ func cleanWebDAVRoot(root string) string { return strings.Trim(root, "/") } +func (a *App) initializeUserWebDAVDefaults() error { + var users []User + if err := a.db.Where("role <> ?", RoleAdmin).Find(&users).Error; err != nil { + return err + } + for _, user := range users { + var cfg WebDAVConfig + err := a.db.Where("user_id = ?", user.ID).First(&cfg).Error + if err == nil { + continue + } + if err != gorm.ErrRecordNotFound { + return err + } + root := cleanWebDAVRoot(strings.TrimRight(a.cfg.WebDAVPath, "/") + "/" + user.Username) + if err := a.db.Create(&WebDAVConfig{UserID: user.ID, Enabled: true, RootPath: root}).Error; err != nil { + return err + } + } + return nil +} + func (a *App) getUserWebDAV(c *gin.Context) { user := c.MustGet("user").(*User) var cfg WebDAVConfig @@ -98,6 +121,10 @@ func (a *App) storageForTask(t *Task) (RemoteStorage, string, bool) { if a.cfg.WebDAVURL == "" || a.storage == nil { return nil, "", false } + var cfg WebDAVConfig + if a.db.Where("user_id = ?", user.ID).First(&cfg).Error == nil && cfg.RootPath != "" { + return a.storage, cfg.RootPath, true + } return a.storage, a.cfg.WebDAVPath, true } if user.Role == RoleAdmin { @@ -107,13 +134,24 @@ func (a *App) storageForTask(t *Task) (RemoteStorage, string, bool) { return a.storage, a.cfg.WebDAVPath, true } var cfg WebDAVConfig - if a.db.Where("user_id = ? AND enabled = ?", user.ID, true).First(&cfg).Error != nil || cfg.URL == "" { + if a.db.Where("user_id = ? AND enabled = ?", user.ID, true).First(&cfg).Error != nil { // Privileged users fall back to the global WebDAV until a custom - // WebDAV configuration is enabled and tested successfully. + // WebDAV configuration is enabled. if a.cfg.WebDAVURL == "" || a.storage == nil { return nil, "", false } return a.storage, a.cfg.WebDAVPath, true } + if cfg.URL == "" { + // An empty URL means this user inherits the global WebDAV connection, + // while keeping the user's own destination path. + if a.cfg.WebDAVURL == "" || a.storage == nil { + return nil, "", false + } + if cfg.RootPath != "" { + return a.storage, cfg.RootPath, true + } + return a.storage, a.cfg.WebDAVPath, true + } return NewWebDAVStorage(Config{WebDAVURL: cfg.URL, WebDAVUser: cfg.Username, WebDAVPass: cfg.Password}), cfg.RootPath, true }