mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2025-08-20 07:02:09 +08:00
object store users
This commit is contained in:
parent
4aec3c3fb9
commit
f47c4aef5a
@ -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
|
||||
|
@ -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"`
|
||||
|
447
weed/admin/dash/user_management.go
Normal file
447
weed/admin/dash/user_management.go
Normal file
@ -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
|
||||
}
|
@ -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")
|
||||
|
255
weed/admin/handlers/user_handlers.go
Normal file
255
weed/admin/handlers/user_handlers.go
Normal file
@ -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(),
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -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>
|
||||
@ -134,23 +131,30 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
<td>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
// User management functions will be included in admin.js
|
||||
</script>
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -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())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
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_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
|
||||
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
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(user.Status)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 139, Col: 64}
|
||||
}
|
||||
_, 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("editUser('%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, 11, "<button type=\"button\" class=\"btn btn-outline-primary btn-sm\" title=\"Edit User\" 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"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 142, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
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
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</td><td>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"><i class=\"fas fa-edit\"></i></button> ")
|
||||
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"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 143, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
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, 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, 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_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_7745c5c3_Err
|
||||
}
|
||||
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 = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("deleteUser('%s')", user.Username)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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_7745c5c3_Err
|
||||
}
|
||||
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, 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
|
||||
|
Loading…
Reference in New Issue
Block a user