diff options
| author | chrislu <chris.lu@gmail.com> | 2025-07-02 00:00:23 -0700 |
|---|---|---|
| committer | chrislu <chris.lu@gmail.com> | 2025-07-02 00:00:23 -0700 |
| commit | f47c4aef5a104a8c6ccd011ce441c453c4bebe62 (patch) | |
| tree | 4656f99515b009354fd09caf51711be6c727093d | |
| parent | 4aec3c3fb9ca4be8242792e58cec2fa41a1f6b34 (diff) | |
| download | seaweedfs-f47c4aef5a104a8c6ccd011ce441c453c4bebe62.tar.xz seaweedfs-f47c4aef5a104a8c6ccd011ce441c453c4bebe62.zip | |
object store users
| -rw-r--r-- | weed/admin/dash/admin_server.go | 87 | ||||
| -rw-r--r-- | weed/admin/dash/handler_admin.go | 43 | ||||
| -rw-r--r-- | weed/admin/dash/user_management.go | 447 | ||||
| -rw-r--r-- | weed/admin/handlers/handlers.go | 78 | ||||
| -rw-r--r-- | weed/admin/handlers/user_handlers.go | 255 | ||||
| -rw-r--r-- | weed/admin/static/js/admin.js | 522 | ||||
| -rw-r--r-- | weed/admin/view/app/object_store_users.templ | 193 | ||||
| -rw-r--r-- | weed/admin/view/app/object_store_users_templ.go | 106 |
8 files changed, 1519 insertions, 212 deletions
diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index bcc126374..cff6f3300 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -1,6 +1,7 @@ package dash import ( + "bytes" "context" "fmt" "net/http" @@ -8,10 +9,12 @@ import ( "time" "github.com/seaweedfs/seaweedfs/weed/cluster" + "github.com/seaweedfs/seaweedfs/weed/filer" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/operation" "github.com/seaweedfs/seaweedfs/weed/pb" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" "github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb" "github.com/seaweedfs/seaweedfs/weed/security" @@ -651,41 +654,57 @@ func (s *AdminServer) DeleteS3Bucket(bucketName string) error { }) } -// GetObjectStoreUsers retrieves object store users data +// GetObjectStoreUsers retrieves object store users from identity.json func (s *AdminServer) GetObjectStoreUsers() ([]ObjectStoreUser, error) { - // For now, return mock data since SeaweedFS doesn't have built-in user management - // In a real implementation, this would query the IAM system or user database - users := []ObjectStoreUser{ - { - Username: "admin", - Email: "admin@example.com", - AccessKey: "AKIAIOSFODNN7EXAMPLE", - SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", - Status: "active", - CreatedAt: time.Now().AddDate(0, -1, 0), - LastLogin: time.Now().AddDate(0, 0, -1), - Permissions: []string{"s3:*", "iam:*"}, - }, - { - Username: "readonly", - Email: "readonly@example.com", - AccessKey: "AKIAI44QH8DHBEXAMPLE", - SecretKey: "je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY", - Status: "active", - CreatedAt: time.Now().AddDate(0, -2, 0), - LastLogin: time.Now().AddDate(0, 0, -3), - Permissions: []string{"s3:GetObject", "s3:ListBucket"}, - }, - { - Username: "backup", - Email: "backup@example.com", - AccessKey: "AKIAIGCEVSQ6C2EXAMPLE", - SecretKey: "BnL1dIqRF/+WoWcouZ5e3qthJhEXAMPLEKEY", - Status: "inactive", - CreatedAt: time.Now().AddDate(0, -3, 0), - LastLogin: time.Now().AddDate(0, -1, -15), - Permissions: []string{"s3:PutObject", "s3:GetObject"}, - }, + s3cfg := &iam_pb.S3ApiConfiguration{} + + // Load IAM configuration from filer + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { + if err == filer_pb.ErrNotFound { + // If file doesn't exist, return empty configuration + return nil + } + return err + } + if buf.Len() > 0 { + return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) + } + return nil + }) + + if err != nil { + glog.Errorf("Failed to load IAM configuration: %v", err) + return []ObjectStoreUser{}, nil // Return empty list instead of error for UI + } + + var users []ObjectStoreUser + + // Convert IAM identities to ObjectStoreUser format + for _, identity := range s3cfg.Identities { + // Skip anonymous identity + if identity.Name == "anonymous" { + continue + } + + user := ObjectStoreUser{ + Username: identity.Name, + Permissions: identity.Actions, + } + + // Set email from account if available + if identity.Account != nil { + user.Email = identity.Account.EmailAddress + } + + // Get first access key for display + if len(identity.Credentials) > 0 { + user.AccessKey = identity.Credentials[0].AccessKey + user.SecretKey = identity.Credentials[0].SecretKey + } + + users = append(users, user) } return users, nil diff --git a/weed/admin/dash/handler_admin.go b/weed/admin/dash/handler_admin.go index a7a783aaf..ae218de75 100644 --- a/weed/admin/dash/handler_admin.go +++ b/weed/admin/dash/handler_admin.go @@ -27,14 +27,11 @@ type AdminData struct { // Object Store Users management structures type ObjectStoreUser struct { - Username string `json:"username"` - Email string `json:"email"` - AccessKey string `json:"access_key"` - SecretKey string `json:"secret_key"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - LastLogin time.Time `json:"last_login"` - Permissions []string `json:"permissions"` + Username string `json:"username"` + Email string `json:"email"` + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + Permissions []string `json:"permissions"` } type ObjectStoreUsersData struct { @@ -44,6 +41,36 @@ type ObjectStoreUsersData struct { LastUpdated time.Time `json:"last_updated"` } +// User management request structures +type CreateUserRequest struct { + Username string `json:"username" binding:"required"` + Email string `json:"email"` + Actions []string `json:"actions"` + GenerateKey bool `json:"generate_key"` +} + +type UpdateUserRequest struct { + Email string `json:"email"` + Actions []string `json:"actions"` +} + +type UpdateUserPoliciesRequest struct { + Actions []string `json:"actions" binding:"required"` +} + +type AccessKeyInfo struct { + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + CreatedAt time.Time `json:"created_at"` +} + +type UserDetails struct { + Username string `json:"username"` + Email string `json:"email"` + Actions []string `json:"actions"` + AccessKeys []AccessKeyInfo `json:"access_keys"` +} + type FilerNode struct { Address string `json:"address"` DataCenter string `json:"datacenter"` diff --git a/weed/admin/dash/user_management.go b/weed/admin/dash/user_management.go new file mode 100644 index 000000000..007faeed8 --- /dev/null +++ b/weed/admin/dash/user_management.go @@ -0,0 +1,447 @@ +package dash + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "fmt" + "time" + + "github.com/seaweedfs/seaweedfs/weed/filer" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" +) + +// CreateObjectStoreUser creates a new user in identity.json +func (s *AdminServer) CreateObjectStoreUser(req CreateUserRequest) (*ObjectStoreUser, error) { + s3cfg := &iam_pb.S3ApiConfiguration{} + + // Load existing configuration + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { + if err != filer_pb.ErrNotFound { + return err + } + } + if buf.Len() > 0 { + return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) + } + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to load IAM configuration: %v", err) + } + + // Check if user already exists + for _, identity := range s3cfg.Identities { + if identity.Name == req.Username { + return nil, fmt.Errorf("user %s already exists", req.Username) + } + } + + // Create new identity + newIdentity := &iam_pb.Identity{ + Name: req.Username, + Actions: req.Actions, + } + + // Add account if email is provided + if req.Email != "" { + newIdentity.Account = &iam_pb.Account{ + Id: generateAccountId(), + DisplayName: req.Username, + EmailAddress: req.Email, + } + } + + // Generate access key if requested + var accessKey, secretKey string + if req.GenerateKey { + accessKey = generateAccessKey() + secretKey = generateSecretKey() + newIdentity.Credentials = []*iam_pb.Credential{ + { + AccessKey: accessKey, + SecretKey: secretKey, + }, + } + } + + // Add to configuration + s3cfg.Identities = append(s3cfg.Identities, newIdentity) + + // Save configuration + err = s.saveS3Configuration(s3cfg) + if err != nil { + return nil, fmt.Errorf("failed to save IAM configuration: %v", err) + } + + // Return created user + user := &ObjectStoreUser{ + Username: req.Username, + Email: req.Email, + AccessKey: accessKey, + SecretKey: secretKey, + Permissions: req.Actions, + } + + return user, nil +} + +// UpdateObjectStoreUser updates an existing user +func (s *AdminServer) UpdateObjectStoreUser(username string, req UpdateUserRequest) (*ObjectStoreUser, error) { + s3cfg := &iam_pb.S3ApiConfiguration{} + + // Load existing configuration + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { + return err + } + if buf.Len() > 0 { + return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) + } + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to load IAM configuration: %v", err) + } + + // Find and update user + var updatedIdentity *iam_pb.Identity + for _, identity := range s3cfg.Identities { + if identity.Name == username { + updatedIdentity = identity + break + } + } + + if updatedIdentity == nil { + return nil, fmt.Errorf("user %s not found", username) + } + + // Update actions if provided + if len(req.Actions) > 0 { + updatedIdentity.Actions = req.Actions + } + + // Update email if provided + if req.Email != "" { + if updatedIdentity.Account == nil { + updatedIdentity.Account = &iam_pb.Account{ + Id: generateAccountId(), + DisplayName: username, + } + } + updatedIdentity.Account.EmailAddress = req.Email + } + + // Save configuration + err = s.saveS3Configuration(s3cfg) + if err != nil { + return nil, fmt.Errorf("failed to save IAM configuration: %v", err) + } + + // Return updated user + user := &ObjectStoreUser{ + Username: username, + Email: req.Email, + Permissions: updatedIdentity.Actions, + } + + // Get first access key for display + if len(updatedIdentity.Credentials) > 0 { + user.AccessKey = updatedIdentity.Credentials[0].AccessKey + user.SecretKey = updatedIdentity.Credentials[0].SecretKey + } + + return user, nil +} + +// DeleteObjectStoreUser deletes a user from identity.json +func (s *AdminServer) DeleteObjectStoreUser(username string) error { + s3cfg := &iam_pb.S3ApiConfiguration{} + + // Load existing configuration + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { + return err + } + if buf.Len() > 0 { + return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) + } + return nil + }) + + if err != nil { + return fmt.Errorf("failed to load IAM configuration: %v", err) + } + + // Find and remove user + found := false + for i, identity := range s3cfg.Identities { + if identity.Name == username { + s3cfg.Identities = append(s3cfg.Identities[:i], s3cfg.Identities[i+1:]...) + found = true + break + } + } + + if !found { + return fmt.Errorf("user %s not found", username) + } + + // Save configuration + return s.saveS3Configuration(s3cfg) +} + +// GetObjectStoreUserDetails returns detailed information about a user +func (s *AdminServer) GetObjectStoreUserDetails(username string) (*UserDetails, error) { + s3cfg := &iam_pb.S3ApiConfiguration{} + + // Load existing configuration + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { + return err + } + if buf.Len() > 0 { + return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) + } + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to load IAM configuration: %v", err) + } + + // Find user + for _, identity := range s3cfg.Identities { + if identity.Name == username { + details := &UserDetails{ + Username: username, + Actions: identity.Actions, + } + + // Set email from account if available + if identity.Account != nil { + details.Email = identity.Account.EmailAddress + } + + // Convert credentials to access key info + for _, cred := range identity.Credentials { + details.AccessKeys = append(details.AccessKeys, AccessKeyInfo{ + AccessKey: cred.AccessKey, + SecretKey: cred.SecretKey, + CreatedAt: time.Now().AddDate(0, -1, 0), // Mock creation date + }) + } + + return details, nil + } + } + + return nil, fmt.Errorf("user %s not found", username) +} + +// CreateAccessKey creates a new access key for a user +func (s *AdminServer) CreateAccessKey(username string) (*AccessKeyInfo, error) { + s3cfg := &iam_pb.S3ApiConfiguration{} + + // Load existing configuration + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { + return err + } + if buf.Len() > 0 { + return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) + } + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to load IAM configuration: %v", err) + } + + // Find user + var targetIdentity *iam_pb.Identity + for _, identity := range s3cfg.Identities { + if identity.Name == username { + targetIdentity = identity + break + } + } + + if targetIdentity == nil { + return nil, fmt.Errorf("user %s not found", username) + } + + // Generate new access key + accessKey := generateAccessKey() + secretKey := generateSecretKey() + + newCredential := &iam_pb.Credential{ + AccessKey: accessKey, + SecretKey: secretKey, + } + + // Add to user's credentials + targetIdentity.Credentials = append(targetIdentity.Credentials, newCredential) + + // Save configuration + err = s.saveS3Configuration(s3cfg) + if err != nil { + return nil, fmt.Errorf("failed to save IAM configuration: %v", err) + } + + return &AccessKeyInfo{ + AccessKey: accessKey, + SecretKey: secretKey, + CreatedAt: time.Now(), + }, nil +} + +// DeleteAccessKey deletes an access key for a user +func (s *AdminServer) DeleteAccessKey(username, accessKeyId string) error { + s3cfg := &iam_pb.S3ApiConfiguration{} + + // Load existing configuration + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { + return err + } + if buf.Len() > 0 { + return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) + } + return nil + }) + + if err != nil { + return fmt.Errorf("failed to load IAM configuration: %v", err) + } + + // Find user and remove access key + for _, identity := range s3cfg.Identities { + if identity.Name == username { + for i, cred := range identity.Credentials { + if cred.AccessKey == accessKeyId { + identity.Credentials = append(identity.Credentials[:i], identity.Credentials[i+1:]...) + return s.saveS3Configuration(s3cfg) + } + } + return fmt.Errorf("access key %s not found for user %s", accessKeyId, username) + } + } + + return fmt.Errorf("user %s not found", username) +} + +// GetUserPolicies returns the policies for a user (actions) +func (s *AdminServer) GetUserPolicies(username string) ([]string, error) { + s3cfg := &iam_pb.S3ApiConfiguration{} + + // Load existing configuration + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { + return err + } + if buf.Len() > 0 { + return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) + } + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to load IAM configuration: %v", err) + } + + // Find user and return policies + for _, identity := range s3cfg.Identities { + if identity.Name == username { + return identity.Actions, nil + } + } + + return nil, fmt.Errorf("user %s not found", username) +} + +// UpdateUserPolicies updates the policies (actions) for a user +func (s *AdminServer) UpdateUserPolicies(username string, actions []string) error { + s3cfg := &iam_pb.S3ApiConfiguration{} + + // Load existing configuration + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { + return err + } + if buf.Len() > 0 { + return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) + } + return nil + }) + + if err != nil { + return fmt.Errorf("failed to load IAM configuration: %v", err) + } + + // Find user and update policies + for _, identity := range s3cfg.Identities { + if identity.Name == username { + identity.Actions = actions + return s.saveS3Configuration(s3cfg) + } + } + + return fmt.Errorf("user %s not found", username) +} + +// saveS3Configuration saves the S3 configuration to identity.json +func (s *AdminServer) saveS3Configuration(s3cfg *iam_pb.S3ApiConfiguration) error { + return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ProtoToText(&buf, s3cfg); err != nil { + return fmt.Errorf("failed to marshal configuration: %v", err) + } + + return filer.SaveInsideFiler(client, filer.IamConfigDirectory, filer.IamIdentityFile, buf.Bytes()) + }) +} + +// Helper functions for generating keys and IDs +func generateAccessKey() string { + // Generate 20-character access key (AWS standard) + const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, 20) + for i := range b { + b[i] = charset[randomInt(len(charset))] + } + return string(b) +} + +func generateSecretKey() string { + // Generate 40-character secret key (AWS standard) + b := make([]byte, 30) // 30 bytes = 40 characters in base64 + rand.Read(b) + return base64.StdEncoding.EncodeToString(b) +} + +func generateAccountId() string { + // Generate 12-digit account ID + b := make([]byte, 8) + rand.Read(b) + return fmt.Sprintf("%012d", b[0]<<24|b[1]<<16|b[2]<<8|b[3]) +} + +func randomInt(max int) int { + b := make([]byte, 1) + rand.Read(b) + return int(b[0]) % max +} diff --git a/weed/admin/handlers/handlers.go b/weed/admin/handlers/handlers.go index ba1cf8e0d..a0140a25d 100644 --- a/weed/admin/handlers/handlers.go +++ b/weed/admin/handlers/handlers.go @@ -16,6 +16,7 @@ type AdminHandlers struct { authHandlers *AuthHandlers clusterHandlers *ClusterHandlers fileBrowserHandlers *FileBrowserHandlers + userHandlers *UserHandlers } // NewAdminHandlers creates a new instance of AdminHandlers @@ -23,11 +24,13 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers { authHandlers := NewAuthHandlers(adminServer) clusterHandlers := NewClusterHandlers(adminServer) fileBrowserHandlers := NewFileBrowserHandlers(adminServer) + userHandlers := NewUserHandlers(adminServer) return &AdminHandlers{ adminServer: adminServer, authHandlers: authHandlers, clusterHandlers: clusterHandlers, fileBrowserHandlers: fileBrowserHandlers, + userHandlers: userHandlers, } } @@ -53,7 +56,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, // Object Store management routes protected.GET("/object-store/buckets", h.ShowS3Buckets) protected.GET("/object-store/buckets/:bucket", h.ShowBucketDetails) - protected.GET("/object-store/users", h.ShowObjectStoreUsers) + protected.GET("/object-store/users", h.userHandlers.ShowObjectStoreUsers) // File browser routes protected.GET("/files", h.fileBrowserHandlers.ShowFileBrowser) @@ -83,6 +86,20 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, s3Api.PUT("/buckets/:bucket/quota", h.adminServer.UpdateBucketQuota) } + // User management API routes + usersApi := api.Group("/users") + { + usersApi.GET("", h.userHandlers.GetUsers) + usersApi.POST("", h.userHandlers.CreateUser) + usersApi.GET("/:username", h.userHandlers.GetUserDetails) + usersApi.PUT("/:username", h.userHandlers.UpdateUser) + usersApi.DELETE("/:username", h.userHandlers.DeleteUser) + usersApi.POST("/:username/access-keys", h.userHandlers.CreateAccessKey) + usersApi.DELETE("/:username/access-keys/:accessKeyId", h.userHandlers.DeleteAccessKey) + usersApi.GET("/:username/policies", h.userHandlers.GetUserPolicies) + usersApi.PUT("/:username/policies", h.userHandlers.UpdateUserPolicies) + } + // File management API routes filesApi := api.Group("/files") { @@ -103,7 +120,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, // Object Store management routes r.GET("/object-store/buckets", h.ShowS3Buckets) r.GET("/object-store/buckets/:bucket", h.ShowBucketDetails) - r.GET("/object-store/users", h.ShowObjectStoreUsers) + r.GET("/object-store/users", h.userHandlers.ShowObjectStoreUsers) // File browser routes r.GET("/files", h.fileBrowserHandlers.ShowFileBrowser) @@ -133,6 +150,20 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, s3Api.PUT("/buckets/:bucket/quota", h.adminServer.UpdateBucketQuota) } + // User management API routes + usersApi := api.Group("/users") + { + usersApi.GET("", h.userHandlers.GetUsers) + usersApi.POST("", h.userHandlers.CreateUser) + usersApi.GET("/:username", h.userHandlers.GetUserDetails) + usersApi.PUT("/:username", h.userHandlers.UpdateUser) + usersApi.DELETE("/:username", h.userHandlers.DeleteUser) + usersApi.POST("/:username/access-keys", h.userHandlers.CreateAccessKey) + usersApi.DELETE("/:username/access-keys/:accessKeyId", h.userHandlers.DeleteAccessKey) + usersApi.GET("/:username/policies", h.userHandlers.GetUserPolicies) + usersApi.PUT("/:username/policies", h.userHandlers.UpdateUserPolicies) + } + // File management API routes filesApi := api.Group("/files") { @@ -196,22 +227,6 @@ func (h *AdminHandlers) ShowBucketDetails(c *gin.Context) { c.JSON(http.StatusOK, details) } -// ShowObjectStoreUsers renders the object store users management page -func (h *AdminHandlers) ShowObjectStoreUsers(c *gin.Context) { - // Get object store users data from the server - usersData := h.getObjectStoreUsersData(c) - - // Render HTML template - c.Header("Content-Type", "text/html") - usersComponent := app.ObjectStoreUsers(usersData) - layoutComponent := layout.Layout(c, usersComponent) - err := layoutComponent.Render(c.Request.Context(), c.Writer) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) - return - } -} - // getS3BucketsData retrieves Object Store buckets data from the server func (h *AdminHandlers) getS3BucketsData(c *gin.Context) dash.S3BucketsData { username := c.GetString("username") @@ -247,33 +262,6 @@ func (h *AdminHandlers) getS3BucketsData(c *gin.Context) dash.S3BucketsData { } } -// getObjectStoreUsersData retrieves object store users data from the server -func (h *AdminHandlers) getObjectStoreUsersData(c *gin.Context) dash.ObjectStoreUsersData { - username := c.GetString("username") - if username == "" { - username = "admin" - } - - // Get object store users - users, err := h.adminServer.GetObjectStoreUsers() - if err != nil { - // Return empty data on error - return dash.ObjectStoreUsersData{ - Username: username, - Users: []dash.ObjectStoreUser{}, - TotalUsers: 0, - LastUpdated: time.Now(), - } - } - - return dash.ObjectStoreUsersData{ - Username: username, - Users: users, - TotalUsers: len(users), - LastUpdated: time.Now(), - } -} - // getAdminData retrieves admin data from the server (now uses consolidated method) func (h *AdminHandlers) getAdminData(c *gin.Context) dash.AdminData { username := c.GetString("username") diff --git a/weed/admin/handlers/user_handlers.go b/weed/admin/handlers/user_handlers.go new file mode 100644 index 000000000..9f36848c0 --- /dev/null +++ b/weed/admin/handlers/user_handlers.go @@ -0,0 +1,255 @@ +package handlers + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/seaweedfs/seaweedfs/weed/admin/dash" + "github.com/seaweedfs/seaweedfs/weed/admin/view/app" + "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" + "github.com/seaweedfs/seaweedfs/weed/glog" +) + +// UserHandlers contains all the HTTP handlers for user management +type UserHandlers struct { + adminServer *dash.AdminServer +} + +// NewUserHandlers creates a new instance of UserHandlers +func NewUserHandlers(adminServer *dash.AdminServer) *UserHandlers { + return &UserHandlers{ + adminServer: adminServer, + } +} + +// ShowObjectStoreUsers renders the object store users management page +func (h *UserHandlers) ShowObjectStoreUsers(c *gin.Context) { + // Get object store users data from the server + usersData := h.getObjectStoreUsersData(c) + + // Render HTML template + c.Header("Content-Type", "text/html") + usersComponent := app.ObjectStoreUsers(usersData) + layoutComponent := layout.Layout(c, usersComponent) + err := layoutComponent.Render(c.Request.Context(), c.Writer) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) + return + } +} + +// GetUsers returns the list of users as JSON +func (h *UserHandlers) GetUsers(c *gin.Context) { + users, err := h.adminServer.GetObjectStoreUsers() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get users: " + err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"users": users}) +} + +// CreateUser handles user creation +func (h *UserHandlers) CreateUser(c *gin.Context) { + var req dash.CreateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + // Validate required fields + if req.Username == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"}) + return + } + + user, err := h.adminServer.CreateObjectStoreUser(req) + if err != nil { + glog.Errorf("Failed to create user %s: %v", req.Username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user: " + err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "message": "User created successfully", + "user": user, + }) +} + +// UpdateUser handles user updates +func (h *UserHandlers) UpdateUser(c *gin.Context) { + username := c.Param("username") + if username == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"}) + return + } + + var req dash.UpdateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + user, err := h.adminServer.UpdateObjectStoreUser(username, req) + if err != nil { + glog.Errorf("Failed to update user %s: %v", username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "User updated successfully", + "user": user, + }) +} + +// DeleteUser handles user deletion +func (h *UserHandlers) DeleteUser(c *gin.Context) { + username := c.Param("username") + if username == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"}) + return + } + + err := h.adminServer.DeleteObjectStoreUser(username) + if err != nil { + glog.Errorf("Failed to delete user %s: %v", username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "User deleted successfully", + }) +} + +// GetUserDetails returns detailed information about a specific user +func (h *UserHandlers) GetUserDetails(c *gin.Context) { + username := c.Param("username") + if username == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"}) + return + } + + user, err := h.adminServer.GetObjectStoreUserDetails(username) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, user) +} + +// CreateAccessKey creates a new access key for a user +func (h *UserHandlers) CreateAccessKey(c *gin.Context) { + username := c.Param("username") + if username == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"}) + return + } + + accessKey, err := h.adminServer.CreateAccessKey(username) + if err != nil { + glog.Errorf("Failed to create access key for user %s: %v", username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create access key: " + err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "message": "Access key created successfully", + "access_key": accessKey, + }) +} + +// DeleteAccessKey deletes an access key for a user +func (h *UserHandlers) DeleteAccessKey(c *gin.Context) { + username := c.Param("username") + accessKeyId := c.Param("accessKeyId") + + if username == "" || accessKeyId == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username and access key ID are required"}) + return + } + + err := h.adminServer.DeleteAccessKey(username, accessKeyId) + if err != nil { + glog.Errorf("Failed to delete access key %s for user %s: %v", accessKeyId, username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete access key: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Access key deleted successfully", + }) +} + +// GetUserPolicies returns the policies for a user +func (h *UserHandlers) GetUserPolicies(c *gin.Context) { + username := c.Param("username") + if username == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"}) + return + } + + policies, err := h.adminServer.GetUserPolicies(username) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user policies: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"policies": policies}) +} + +// UpdateUserPolicies updates the policies for a user +func (h *UserHandlers) UpdateUserPolicies(c *gin.Context) { + username := c.Param("username") + if username == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"}) + return + } + + var req dash.UpdateUserPoliciesRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + err := h.adminServer.UpdateUserPolicies(username, req.Actions) + if err != nil { + glog.Errorf("Failed to update policies for user %s: %v", username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user policies: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "User policies updated successfully", + }) +} + +// getObjectStoreUsersData retrieves object store users data from the server +func (h *UserHandlers) getObjectStoreUsersData(c *gin.Context) dash.ObjectStoreUsersData { + username := c.GetString("username") + if username == "" { + username = "admin" + } + + // Get object store users + users, err := h.adminServer.GetObjectStoreUsers() + if err != nil { + glog.Errorf("Failed to get object store users: %v", err) + // Return empty data on error + return dash.ObjectStoreUsersData{ + Username: username, + Users: []dash.ObjectStoreUser{}, + TotalUsers: 0, + LastUpdated: time.Now(), + } + } + + return dash.ObjectStoreUsersData{ + Username: username, + Users: users, + TotalUsers: len(users), + LastUpdated: time.Now(), + } +} diff --git a/weed/admin/static/js/admin.js b/weed/admin/static/js/admin.js index caca57a12..504e02785 100644 --- a/weed/admin/static/js/admin.js +++ b/weed/admin/static/js/admin.js @@ -807,34 +807,30 @@ function exportUsers() { showAlert('error', 'Users table not found'); return; } - - const headers = ['Username', 'Email', 'Access Key', 'Status', 'Created', 'Last Login']; - const rows = []; - - // Get table rows - const tableRows = table.querySelectorAll('tbody tr'); - tableRows.forEach(row => { + + const rows = table.querySelectorAll('tbody tr'); + if (rows.length === 0) { + showErrorMessage('No users to export'); + return; + } + + let csvContent = 'Username,Email,Access Key,Status,Created,Last Login\n'; + + rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length >= 6) { - rows.push([ - cells[0].textContent.trim(), - cells[1].textContent.trim(), - cells[2].textContent.trim(), - cells[3].textContent.trim(), - cells[4].textContent.trim(), - cells[5].textContent.trim() - ]); + const username = cells[0].textContent.trim(); + const email = cells[1].textContent.trim(); + const accessKey = cells[2].textContent.trim(); + const status = cells[3].textContent.trim(); + const created = cells[4].textContent.trim(); + const lastLogin = cells[5].textContent.trim(); + + csvContent += `"${username}","${email}","${accessKey}","${status}","${created}","${lastLogin}"\n`; } }); - - // Generate CSV - const csvContent = [headers, ...rows] - .map(row => row.map(cell => `"${cell}"`).join(',')) - .join('\n'); - - // Download - const filename = `seaweedfs-users-${new Date().toISOString().split('T')[0]}.csv`; - downloadCSV(csvContent, filename); + + downloadCSV(csvContent, 'seaweedfs-users.csv'); } // Confirm delete collection @@ -2062,9 +2058,481 @@ function createPropertiesContent(properties) { // Utility function to escape HTML function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + var map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, function(m) { return map[m]; }); +} + +// ============================================================================ +// USER MANAGEMENT FUNCTIONS +// ============================================================================ + +// Global variables for user management +let currentEditingUser = ''; +let currentAccessKeysUser = ''; + +// User Management Functions + +async function handleCreateUser() { + const form = document.getElementById('createUserForm'); + const formData = new FormData(form); + + // Get selected actions + const actionsSelect = document.getElementById('actions'); + const selectedActions = Array.from(actionsSelect.selectedOptions).map(option => option.value); + + const userData = { + username: formData.get('username'), + email: formData.get('email'), + actions: selectedActions, + generate_key: formData.get('generateKey') === 'on' + }; + + try { + const response = await fetch('/api/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(userData) + }); + + if (response.ok) { + const result = await response.json(); + showSuccessMessage('User created successfully'); + + // Show the created access key if generated + if (result.user && result.user.access_key) { + showNewAccessKeyModal(result.user); + } + + // Close modal and refresh page + const modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal')); + modal.hide(); + form.reset(); + setTimeout(() => window.location.reload(), 1000); + } else { + const error = await response.json(); + showErrorMessage('Failed to create user: ' + (error.error || 'Unknown error')); + } + } catch (error) { + console.error('Error creating user:', error); + showErrorMessage('Failed to create user: ' + error.message); + } } +async function editUser(username) { + currentEditingUser = username; + + try { + const response = await fetch(`/api/users/${username}`); + if (response.ok) { + const user = await response.json(); + + // Populate edit form + document.getElementById('editUsername').value = username; + document.getElementById('editEmail').value = user.email || ''; + + // Set selected actions + const actionsSelect = document.getElementById('editActions'); + Array.from(actionsSelect.options).forEach(option => { + option.selected = user.actions && user.actions.includes(option.value); + }); + + // Show modal + const modal = new bootstrap.Modal(document.getElementById('editUserModal')); + modal.show(); + } else { + showErrorMessage('Failed to load user details'); + } + } catch (error) { + console.error('Error loading user:', error); + showErrorMessage('Failed to load user details'); + } +} + +async function handleUpdateUser() { + const form = document.getElementById('editUserForm'); + const formData = new FormData(form); + + // Get selected actions + const actionsSelect = document.getElementById('editActions'); + const selectedActions = Array.from(actionsSelect.selectedOptions).map(option => option.value); + + const userData = { + email: formData.get('email'), + actions: selectedActions + }; + + try { + const response = await fetch(`/api/users/${currentEditingUser}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(userData) + }); + + if (response.ok) { + showSuccessMessage('User updated successfully'); + + // Close modal and refresh page + const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal')); + modal.hide(); + setTimeout(() => window.location.reload(), 1000); + } else { + const error = await response.json(); + showErrorMessage('Failed to update user: ' + (error.error || 'Unknown error')); + } + } catch (error) { + console.error('Error updating user:', error); + showErrorMessage('Failed to update user: ' + error.message); + } +} + +function confirmDeleteUser(username) { + confirmAction( + `Are you sure you want to delete user "${username}"? This action cannot be undone.`, + () => deleteUserConfirmed(username) + ); +} + +function deleteUser(username) { + confirmDeleteUser(username); +} + +async function deleteUserConfirmed(username) { + try { + const response = await fetch(`/api/users/${username}`, { + method: 'DELETE' + }); + + if (response.ok) { + showSuccessMessage('User deleted successfully'); + setTimeout(() => window.location.reload(), 1000); + } else { + const error = await response.json(); + showErrorMessage('Failed to delete user: ' + (error.error || 'Unknown error')); + } + } catch (error) { + console.error('Error deleting user:', error); + showErrorMessage('Failed to delete user: ' + error.message); + } +} + +async function showUserDetails(username) { + try { + const response = await fetch(`/api/users/${username}`); + if (response.ok) { + const user = await response.json(); + + const content = createUserDetailsContent(user); + document.getElementById('userDetailsContent').innerHTML = content; + + const modal = new bootstrap.Modal(document.getElementById('userDetailsModal')); + modal.show(); + } else { + showErrorMessage('Failed to load user details'); + } + } catch (error) { + console.error('Error loading user details:', error); + showErrorMessage('Failed to load user details'); + } +} + +function createUserDetailsContent(user) { + return ` + <div class="row"> + <div class="col-md-6"> + <h6 class="text-muted">Basic Information</h6> + <table class="table table-sm"> + <tr> + <td><strong>Username:</strong></td> + <td>${escapeHtml(user.username)}</td> + </tr> + <tr> + <td><strong>Email:</strong></td> + <td>${escapeHtml(user.email || 'Not set')}</td> + </tr> + </table> + </div> + <div class="col-md-6"> + <h6 class="text-muted">Permissions</h6> + <div class="mb-3"> + ${user.actions && user.actions.length > 0 ? + user.actions.map(action => `<span class="badge bg-info me-1">${action}</span>`).join('') : + '<span class="text-muted">No permissions assigned</span>' + } + </div> + + <h6 class="text-muted">Access Keys</h6> + ${user.access_keys && user.access_keys.length > 0 ? + createAccessKeysTable(user.access_keys) : + '<p class="text-muted">No access keys</p>' + } + </div> + </div> + `; +} + +function createAccessKeysTable(accessKeys) { + return ` + <div class="table-responsive"> + <table class="table table-sm"> + <thead> + <tr> + <th>Access Key</th> + <th>Created</th> + </tr> + </thead> + <tbody> + ${accessKeys.map(key => ` + <tr> + <td><code>${key.access_key}</code></td> + <td>${new Date(key.created_at).toLocaleDateString()}</td> + </tr> + `).join('')} + </tbody> + </table> + </div> + `; +} + +async function manageAccessKeys(username) { + currentAccessKeysUser = username; + document.getElementById('accessKeysUsername').textContent = username; + + await loadAccessKeys(username); + + const modal = new bootstrap.Modal(document.getElementById('accessKeysModal')); + modal.show(); +} + +async function loadAccessKeys(username) { + try { + const response = await fetch(`/api/users/${username}`); + if (response.ok) { + const user = await response.json(); + + const content = createAccessKeysManagementContent(user.access_keys || []); + document.getElementById('accessKeysContent').innerHTML = content; + } else { + document.getElementById('accessKeysContent').innerHTML = '<p class="text-muted">Failed to load access keys</p>'; + } + } catch (error) { + console.error('Error loading access keys:', error); + document.getElementById('accessKeysContent').innerHTML = '<p class="text-muted">Error loading access keys</p>'; + } +} + +function createAccessKeysManagementContent(accessKeys) { + if (accessKeys.length === 0) { + return '<p class="text-muted">No access keys found. Create one to get started.</p>'; + } + + return ` + <div class="table-responsive"> + <table class="table table-hover"> + <thead> + <tr> + <th>Access Key</th> + <th>Secret Key</th> + <th>Created</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + ${accessKeys.map(key => ` + <tr> + <td> + <code>${key.access_key}</code> + <button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyToClipboard('${key.access_key}')"> + <i class="fas fa-copy"></i> + </button> + </td> + <td> + <code class="text-muted">••••••••••••••••</code> + <button class="btn btn-sm btn-outline-secondary ms-2" onclick="showSecretKey('${key.access_key}', '${key.secret_key}')"> + <i class="fas fa-eye"></i> + </button> + </td> + <td>${new Date(key.created_at).toLocaleDateString()}</td> + <td> + <button class="btn btn-sm btn-outline-danger" onclick="confirmDeleteAccessKey('${key.access_key}')"> + <i class="fas fa-trash"></i> + </button> + </td> + </tr> + `).join('')} + </tbody> + </table> + </div> + `; +} + +async function createAccessKey() { + if (!currentAccessKeysUser) { + showErrorMessage('No user selected'); + return; + } + + try { + const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }); + + if (response.ok) { + const result = await response.json(); + showSuccessMessage('Access key created successfully'); + + // Show the new access key + showNewAccessKeyModal(result.access_key); + + // Reload access keys + await loadAccessKeys(currentAccessKeysUser); + } else { + const error = await response.json(); + showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error')); + } + } catch (error) { + console.error('Error creating access key:', error); + showErrorMessage('Failed to create access key: ' + error.message); + } +} + +function confirmDeleteAccessKey(accessKeyId) { + confirmAction( + `Are you sure you want to delete access key "${accessKeyId}"? This action cannot be undone.`, + () => deleteAccessKeyConfirmed(accessKeyId) + ); +} + +async function deleteAccessKeyConfirmed(accessKeyId) { + try { + const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys/${accessKeyId}`, { + method: 'DELETE' + }); + + if (response.ok) { + showSuccessMessage('Access key deleted successfully'); + + // Reload access keys + await loadAccessKeys(currentAccessKeysUser); + } else { + const error = await response.json(); + showErrorMessage('Failed to delete access key: ' + (error.error || 'Unknown error')); + } + } catch (error) { + console.error('Error deleting access key:', error); + showErrorMessage('Failed to delete access key: ' + error.message); + } +} + +function showSecretKey(accessKey, secretKey) { + const content = ` + <div class="alert alert-info"> + <i class="fas fa-info-circle me-2"></i> + <strong>Access Key Details:</strong> These credentials provide access to your object storage. Keep them secure and don't share them. + </div> + <div class="mb-3"> + <label class="form-label"><strong>Access Key:</strong></label> + <div class="input-group"> + <input type="text" class="form-control" value="${accessKey}" readonly> + <button class="btn btn-outline-secondary" onclick="copyToClipboard('${accessKey}')"> + <i class="fas fa-copy"></i> + </button> + </div> + </div> + <div class="mb-3"> + <label class="form-label"><strong>Secret Key:</strong></label> + <div class="input-group"> + <input type="text" class="form-control" value="${secretKey}" readonly> + <button class="btn btn-outline-secondary" onclick="copyToClipboard('${secretKey}')"> + <i class="fas fa-copy"></i> + </button> + </div> + </div> + `; + + showModal('Access Key Details', content); +} + +function showNewAccessKeyModal(accessKeyData) { + const content = ` + <div class="alert alert-success"> + <i class="fas fa-check-circle me-2"></i> + <strong>Success!</strong> Your new access key has been created. + </div> + <div class="alert alert-info"> + <i class="fas fa-info-circle me-2"></i> + <strong>Important:</strong> These credentials provide access to your object storage. Keep them secure and don't share them. You can view them again through the user management interface if needed. + </div> + <div class="mb-3"> + <label class="form-label"><strong>Access Key:</strong></label> + <div class="input-group"> + <input type="text" class="form-control" value="${accessKeyData.access_key}" readonly> + <button class="btn btn-outline-secondary" onclick="copyToClipboard('${accessKeyData.access_key}')"> + <i class="fas fa-copy"></i> + </button> + </div> + </div> + <div class="mb-3"> + <label class="form-label"><strong>Secret Key:</strong></label> + <div class="input-group"> + <input type="text" class="form-control" value="${accessKeyData.secret_key}" readonly> + <button class="btn btn-outline-secondary" onclick="copyToClipboard('${accessKeyData.secret_key}')"> + <i class="fas fa-copy"></i> + </button> + </div> + </div> + `; + + showModal('New Access Key Created', content); +} + +function showModal(title, content) { + // Create a dynamic modal + const modalId = 'dynamicModal_' + Date.now(); + const modalHtml = ` + <div class="modal fade" id="${modalId}" tabindex="-1" role="dialog"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">${title}</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + ${content} + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> + </div> + </div> + </div> + </div> + `; + + // Add modal to body + document.body.insertAdjacentHTML('beforeend', modalHtml); + + // Show modal + const modal = new bootstrap.Modal(document.getElementById(modalId)); + modal.show(); + + // Remove modal from DOM when hidden + document.getElementById(modalId).addEventListener('hidden.bs.modal', function() { + this.remove(); + }); +} + + +
\ No newline at end of file diff --git a/weed/admin/view/app/object_store_users.templ b/weed/admin/view/app/object_store_users.templ index 2329a0178..4b54d5684 100644 --- a/weed/admin/view/app/object_store_users.templ +++ b/weed/admin/view/app/object_store_users.templ @@ -52,10 +52,10 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { <div class="row no-gutters align-items-center"> <div class="col mr-2"> <div class="text-xs font-weight-bold text-success text-uppercase mb-1"> - Active Users + Total Users </div> <div class="h5 mb-0 font-weight-bold text-gray-800"> - {fmt.Sprintf("%d", countActiveUsers(data.Users))} + {fmt.Sprintf("%d", len(data.Users))} </div> </div> <div class="col-auto"> @@ -115,9 +115,6 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { <th>Username</th> <th>Email</th> <th>Access Key</th> - <th>Status</th> - <th>Created</th> - <th>Last Login</th> <th>Actions</th> </tr> </thead> @@ -135,22 +132,29 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { <code class="text-muted">{user.AccessKey}</code> </td> <td> - <span class={fmt.Sprintf("badge bg-%s", getUserStatusColor(user.Status))}> - {user.Status} - </span> - </td> - <td>{user.CreatedAt.Format("2006-01-02")}</td> - <td>{user.LastLogin.Format("2006-01-02")}</td> - <td> <div class="btn-group btn-group-sm" role="group"> <button type="button" + class="btn btn-outline-info btn-sm" + title="View Details" + onclick={ templ.ComponentScript{Call: fmt.Sprintf("showUserDetails('%s')", user.Username)} }> + <i class="fas fa-eye"></i> + </button> + <button type="button" class="btn btn-outline-primary btn-sm" - title="Edit User"> + title="Edit User" + onclick={ templ.ComponentScript{Call: fmt.Sprintf("editUser('%s')", user.Username)} }> <i class="fas fa-edit"></i> </button> <button type="button" + class="btn btn-outline-warning btn-sm" + title="Manage Keys" + onclick={ templ.ComponentScript{Call: fmt.Sprintf("manageAccessKeys('%s')", user.Username)} }> + <i class="fas fa-key"></i> + </button> + <button type="button" class="btn btn-outline-danger btn-sm" - title="Delete User"> + title="Delete User" + onclick={ templ.ComponentScript{Call: fmt.Sprintf("deleteUser('%s')", user.Username)} }> <i class="fas fa-trash"></i> </button> </div> @@ -159,7 +163,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { } if len(data.Users) == 0 { <tr> - <td colspan="7" class="text-center text-muted py-4"> + <td colspan="4" class="text-center text-muted py-4"> <i class="fas fa-users fa-3x mb-3 text-muted"></i> <div> <h5>No users found</h5> @@ -186,29 +190,144 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { </div> </div> </div> -} -// Helper functions for template -func getUserStatusColor(status string) string { - switch status { - case "active": - return "success" - case "inactive": - return "warning" - case "suspended": - return "danger" - default: - return "secondary" - } -} + <!-- Create User Modal --> + <div class="modal fade" id="createUserModal" tabindex="-1" role="dialog"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title"> + <i class="fas fa-user-plus me-2"></i>Create New User + </h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + <form id="createUserForm"> + <div class="mb-3"> + <label for="username" class="form-label">Username *</label> + <input type="text" class="form-control" id="username" name="username" required> + </div> + <div class="mb-3"> + <label for="email" class="form-label">Email</label> + <input type="email" class="form-control" id="email" name="email"> + </div> + <div class="mb-3"> + <label for="actions" class="form-label">Permissions</label> + <select multiple class="form-control" id="actions" name="actions"> + <option value="Admin">Admin (Full Access)</option> + <option value="Read">Read</option> + <option value="Write">Write</option> + <option value="List">List</option> + <option value="Tagging">Tagging</option> + </select> + <small class="form-text text-muted">Hold Ctrl/Cmd to select multiple permissions</small> + </div> + <div class="mb-3 form-check"> + <input type="checkbox" class="form-check-input" id="generateKey" name="generateKey" checked> + <label class="form-check-label" for="generateKey"> + Generate access key automatically + </label> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-primary" onclick="handleCreateUser()">Create User</button> + </div> + </div> + </div> + </div> -func countActiveUsers(users []dash.ObjectStoreUser) int { - count := 0 - for _, user := range users { - if user.Status == "active" { - count++ - } - } - return count + <!-- Edit User Modal --> + <div class="modal fade" id="editUserModal" tabindex="-1" role="dialog"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title"> + <i class="fas fa-user-edit me-2"></i>Edit User + </h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + <form id="editUserForm"> + <input type="hidden" id="editUsername" name="username"> + <div class="mb-3"> + <label for="editEmail" class="form-label">Email</label> + <input type="email" class="form-control" id="editEmail" name="email"> + </div> + <div class="mb-3"> + <label for="editActions" class="form-label">Permissions</label> + <select multiple class="form-control" id="editActions" name="actions"> + <option value="Admin">Admin (Full Access)</option> + <option value="Read">Read</option> + <option value="Write">Write</option> + <option value="List">List</option> + <option value="Tagging">Tagging</option> + </select> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-primary" onclick="handleUpdateUser()">Update User</button> + </div> + </div> + </div> + </div> + + <!-- User Details Modal --> + <div class="modal fade" id="userDetailsModal" tabindex="-1" role="dialog"> + <div class="modal-dialog modal-lg" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title"> + <i class="fas fa-user me-2"></i>User Details + </h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body" id="userDetailsContent"> + <!-- Content will be loaded dynamically --> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> + </div> + </div> + </div> + </div> + + <!-- Access Keys Management Modal --> + <div class="modal fade" id="accessKeysModal" tabindex="-1" role="dialog"> + <div class="modal-dialog modal-lg" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title"> + <i class="fas fa-key me-2"></i>Manage Access Keys + </h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + <div class="d-flex justify-content-between align-items-center mb-3"> + <h6>Access Keys for <span id="accessKeysUsername"></span></h6> + <button type="button" class="btn btn-primary btn-sm" onclick="createAccessKey()"> + <i class="fas fa-plus me-1"></i>Create New Key + </button> + </div> + <div id="accessKeysContent"> + <!-- Content will be loaded dynamically --> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> + </div> + </div> + </div> + </div> + + <!-- JavaScript for user management --> + <script> + // User management functions will be included in admin.js + </script> } + +// Helper functions for template
\ No newline at end of file diff --git a/weed/admin/view/app/object_store_users_templ.go b/weed/admin/view/app/object_store_users_templ.go index d2af6ea27..83f5aa948 100644 --- a/weed/admin/view/app/object_store_users_templ.go +++ b/weed/admin/view/app/object_store_users_templ.go @@ -47,14 +47,14 @@ func ObjectStoreUsers(data dash.ObjectStoreUsersData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div></div><div class=\"col-auto\"><i class=\"fas fa-users fa-2x text-gray-300\"></i></div></div></div></div></div><div class=\"col-xl-3 col-md-6 mb-4\"><div class=\"card border-left-success shadow h-100 py-2\"><div class=\"card-body\"><div class=\"row no-gutters align-items-center\"><div class=\"col mr-2\"><div class=\"text-xs font-weight-bold text-success text-uppercase mb-1\">Active Users</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div></div><div class=\"col-auto\"><i class=\"fas fa-users fa-2x text-gray-300\"></i></div></div></div></div></div><div class=\"col-xl-3 col-md-6 mb-4\"><div class=\"card border-left-success shadow h-100 py-2\"><div class=\"card-body\"><div class=\"row no-gutters align-items-center\"><div class=\"col mr-2\"><div class=\"text-xs font-weight-bold text-success text-uppercase mb-1\">Total Users</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var3 string - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", countActiveUsers(data.Users))) + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(data.Users))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 58, Col: 84} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 58, Col: 71} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -73,7 +73,7 @@ func ObjectStoreUsers(data dash.ObjectStoreUsersData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div><div class=\"col-auto\"><i class=\"fas fa-clock fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- Users Table --><div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3 d-flex flex-row align-items-center justify-content-between\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-users me-2\"></i>Object Store Users</h6><div class=\"dropdown no-arrow\"><a class=\"dropdown-toggle\" href=\"#\" role=\"button\" data-bs-toggle=\"dropdown\"><i class=\"fas fa-ellipsis-v fa-sm fa-fw text-gray-400\"></i></a><div class=\"dropdown-menu dropdown-menu-right shadow animated--fade-in\"><div class=\"dropdown-header\">Actions:</div><a class=\"dropdown-item\" href=\"#\" onclick=\"exportUsers()\"><i class=\"fas fa-download me-2\"></i>Export List</a></div></div></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-hover\" width=\"100%\" cellspacing=\"0\" id=\"usersTable\"><thead><tr><th>Username</th><th>Email</th><th>Access Key</th><th>Status</th><th>Created</th><th>Last Login</th><th>Actions</th></tr></thead> <tbody>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div><div class=\"col-auto\"><i class=\"fas fa-clock fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- Users Table --><div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3 d-flex flex-row align-items-center justify-content-between\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-users me-2\"></i>Object Store Users</h6><div class=\"dropdown no-arrow\"><a class=\"dropdown-toggle\" href=\"#\" role=\"button\" data-bs-toggle=\"dropdown\"><i class=\"fas fa-ellipsis-v fa-sm fa-fw text-gray-400\"></i></a><div class=\"dropdown-menu dropdown-menu-right shadow animated--fade-in\"><div class=\"dropdown-header\">Actions:</div><a class=\"dropdown-item\" href=\"#\" onclick=\"exportUsers()\"><i class=\"fas fa-download me-2\"></i>Export List</a></div></div></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-hover\" width=\"100%\" cellspacing=\"0\" id=\"usersTable\"><thead><tr><th>Username</th><th>Email</th><th>Access Key</th><th>Actions</th></tr></thead> <tbody>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -85,7 +85,7 @@ func ObjectStoreUsers(data dash.ObjectStoreUsersData) templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 130, Col: 74} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 127, Col: 74} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -98,7 +98,7 @@ func ObjectStoreUsers(data dash.ObjectStoreUsersData) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 133, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 130, Col: 59} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -111,98 +111,105 @@ func ObjectStoreUsers(data dash.ObjectStoreUsersData) templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(user.AccessKey) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 135, Col: 88} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 132, Col: 88} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</code></td><td>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</code></td><td><div class=\"btn-group btn-group-sm\" role=\"group\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var8 = []any{fmt.Sprintf("badge bg-%s", getUserStatusColor(user.Status))} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("showUserDetails('%s')", user.Username)}) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span class=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<button type=\"button\" class=\"btn btn-outline-info btn-sm\" title=\"View Details\" onclick=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String()) + var templ_7745c5c3_Var8 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("showUserDetails('%s')", user.Username)} + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8.Call) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 1, Col: 0} + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"><i class=\"fas fa-eye\"></i></button> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("editUser('%s')", user.Username)}) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<button type=\"button\" class=\"btn btn-outline-primary btn-sm\" title=\"Edit User\" onclick=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">") + var templ_7745c5c3_Var9 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("editUser('%s')", user.Username)} + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9.Call) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(user.Status) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"><i class=\"fas fa-edit\"></i></button> ") if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 139, Col: 64} + return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("manageAccessKeys('%s')", user.Username)}) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></td><td>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<button type=\"button\" class=\"btn btn-outline-warning btn-sm\" title=\"Manage Keys\" onclick=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(user.CreatedAt.Format("2006-01-02")) + var templ_7745c5c3_Var10 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("manageAccessKeys('%s')", user.Username)} + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10.Call) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 142, Col: 84} + return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"><i class=\"fas fa-key\"></i></button> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</td><td>") + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("deleteUser('%s')", user.Username)}) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(user.LastLogin.Format("2006-01-02")) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<button type=\"button\" class=\"btn btn-outline-danger btn-sm\" title=\"Delete User\" onclick=\"") if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 143, Col: 84} + return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + var templ_7745c5c3_Var11 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("deleteUser('%s')", user.Username)} + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11.Call) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</td><td><div class=\"btn-group btn-group-sm\" role=\"group\"><button type=\"button\" class=\"btn btn-outline-primary btn-sm\" title=\"Edit User\"><i class=\"fas fa-edit\"></i></button> <button type=\"button\" class=\"btn btn-outline-danger btn-sm\" title=\"Delete User\"><i class=\"fas fa-trash\"></i></button></div></td></tr>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"><i class=\"fas fa-trash\"></i></button></div></td></tr>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(data.Users) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<tr><td colspan=\"7\" class=\"text-center text-muted py-4\"><i class=\"fas fa-users fa-3x mb-3 text-muted\"></i><div><h5>No users found</h5><p>Create your first object store user to get started.</p></div></td></tr>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<tr><td colspan=\"4\" class=\"text-center text-muted py-4\"><i class=\"fas fa-users fa-3x mb-3 text-muted\"></i><div><h5>No users found</h5><p>Create your first object store user to get started.</p></div></td></tr>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</tbody></table></div></div></div></div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"fas fa-clock me-1\"></i> Last updated: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</tbody></table></div></div></div></div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"fas fa-clock me-1\"></i> Last updated: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 184, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 188, Col: 81} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</small></div></div></div>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</small></div></div></div><!-- Create User Modal --><div class=\"modal fade\" id=\"createUserModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-user-plus me-2\"></i>Create New User</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\"><form id=\"createUserForm\"><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Username *</label> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div><div class=\"mb-3\"><label for=\"email\" class=\"form-label\">Email</label> <input type=\"email\" class=\"form-control\" id=\"email\" name=\"email\"></div><div class=\"mb-3\"><label for=\"actions\" class=\"form-label\">Permissions</label> <select multiple class=\"form-control\" id=\"actions\" name=\"actions\"><option value=\"Admin\">Admin (Full Access)</option> <option value=\"Read\">Read</option> <option value=\"Write\">Write</option> <option value=\"List\">List</option> <option value=\"Tagging\">Tagging</option></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple permissions</small></div><div class=\"mb-3 form-check\"><input type=\"checkbox\" class=\"form-check-input\" id=\"generateKey\" name=\"generateKey\" checked> <label class=\"form-check-label\" for=\"generateKey\">Generate access key automatically</label></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"handleCreateUser()\">Create User</button></div></div></div></div><!-- Edit User Modal --><div class=\"modal fade\" id=\"editUserModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-user-edit me-2\"></i>Edit User</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\"><form id=\"editUserForm\"><input type=\"hidden\" id=\"editUsername\" name=\"username\"><div class=\"mb-3\"><label for=\"editEmail\" class=\"form-label\">Email</label> <input type=\"email\" class=\"form-control\" id=\"editEmail\" name=\"email\"></div><div class=\"mb-3\"><label for=\"editActions\" class=\"form-label\">Permissions</label> <select multiple class=\"form-control\" id=\"editActions\" name=\"actions\"><option value=\"Admin\">Admin (Full Access)</option> <option value=\"Read\">Read</option> <option value=\"Write\">Write</option> <option value=\"List\">List</option> <option value=\"Tagging\">Tagging</option></select></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"handleUpdateUser()\">Update User</button></div></div></div></div><!-- User Details Modal --><div class=\"modal fade\" id=\"userDetailsModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog modal-lg\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-user me-2\"></i>User Details</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\" id=\"userDetailsContent\"><!-- Content will be loaded dynamically --></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button></div></div></div></div><!-- Access Keys Management Modal --><div class=\"modal fade\" id=\"accessKeysModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog modal-lg\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-key me-2\"></i>Manage Access Keys</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\"><div class=\"d-flex justify-content-between align-items-center mb-3\"><h6>Access Keys for <span id=\"accessKeysUsername\"></span></h6><button type=\"button\" class=\"btn btn-primary btn-sm\" onclick=\"createAccessKey()\"><i class=\"fas fa-plus me-1\"></i>Create New Key</button></div><div id=\"accessKeysContent\"><!-- Content will be loaded dynamically --></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button></div></div></div></div><!-- JavaScript for user management --><script>\n // User management functions will be included in admin.js\n </script>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -211,27 +218,4 @@ func ObjectStoreUsers(data dash.ObjectStoreUsersData) templ.Component { } // Helper functions for template -func getUserStatusColor(status string) string { - switch status { - case "active": - return "success" - case "inactive": - return "warning" - case "suspended": - return "danger" - default: - return "secondary" - } -} - -func countActiveUsers(users []dash.ObjectStoreUser) int { - count := 0 - for _, user := range users { - if user.Status == "active" { - count++ - } - } - return count -} - var _ = templruntime.GeneratedTemplate |
