From df39550eb13eba35cb909753660b0bae0c79f3f7 Mon Sep 17 00:00:00 2001 From: ats-tech25 Date: Thu, 6 Nov 2025 09:13:26 +0000 Subject: [PATCH] feat(admin): Implement comprehensive CLI admin management functionality - Add new `internal/cli/admin.go` package for admin management - Implement interactive admin account creation with secure password input - Add CLI command for creating admin accounts with flexible input options - Implement validation for admin account creation details - Support both interactive and flag-based admin account creation - Integrate with existing user and authentication services - Update go.mod and go.sum with new dependencies and version upgrades Enhances system administration capabilities by providing a flexible CLI tool for creating admin accounts with robust security and usability features. --- go.mod | 3 +- go.sum | 4 + internal/cli/admin.go | 212 ++++++++++++++++++++++++++++++ internal/services/interfaces.go | 8 ++ internal/services/user_service.go | 45 +++++++ 5 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 internal/cli/admin.go diff --git a/go.mod b/go.mod index 54a3916..e5f8b91 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,8 @@ require ( golang.org/x/arch v0.3.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 271b8b6..0c1c53a 100644 --- a/go.sum +++ b/go.sum @@ -109,7 +109,11 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= diff --git a/internal/cli/admin.go b/internal/cli/admin.go new file mode 100644 index 0000000..e31084c --- /dev/null +++ b/internal/cli/admin.go @@ -0,0 +1,212 @@ +package cli + +import ( + "bufio" + "fmt" + "log" + "os" + "strings" + "syscall" + + "attune-heart-therapy/internal/config" + "attune-heart-therapy/internal/database" + "attune-heart-therapy/internal/services" + + "github.com/joho/godotenv" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" +) + +var adminCmd = &cobra.Command{ + Use: "admin", + Short: "Admin management commands", + Long: "Commands for managing admin accounts and operations", +} + +var createAdminCmd = &cobra.Command{ + Use: "create-admin", + Short: "Create a new admin account", + Long: "Create a new admin account with email and password parameters", + Run: runCreateAdmin, +} + +// Command flags +var ( + adminEmail string + adminPassword string + adminFirstName string + adminLastName string + interactive bool +) + +func runCreateAdmin(cmd *cobra.Command, args []string) { + // Load environment variables + if err := godotenv.Load(); err != nil { + log.Println("No .env file found, using system environment variables") + } + + // Load configuration + cfg, err := config.Load() + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + } + + // Initialize database connection + db, err := database.New(cfg) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + defer db.Close() + + // Get repositories + repos := db.GetRepositories() + + // Initialize services + jwtService := services.NewJWTService(cfg.JWT.Secret, cfg.JWT.Expiration) + notificationService := services.NewNotificationService(repos.Notification, cfg) + userService := services.NewUserService(repos.User, jwtService, notificationService) + + // Get admin details + if interactive || adminEmail == "" || adminPassword == "" { + if err := getAdminDetailsInteractively(); err != nil { + log.Fatalf("Failed to get admin details: %v", err) + } + } + + // Validate required fields + if err := validateAdminInput(); err != nil { + log.Fatalf("Validation failed: %v", err) + } + + // Create admin account + if err := createAdminAccount(userService); err != nil { + log.Fatalf("Failed to create admin account: %v", err) + } + + fmt.Printf("Admin account created successfully for: %s\n", adminEmail) +} + +func getAdminDetailsInteractively() error { + reader := bufio.NewReader(os.Stdin) + + // Get email + if adminEmail == "" { + fmt.Print("Enter admin email: ") + email, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read email: %w", err) + } + adminEmail = strings.TrimSpace(email) + } + + // Get first name + if adminFirstName == "" { + fmt.Print("Enter admin first name: ") + firstName, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read first name: %w", err) + } + adminFirstName = strings.TrimSpace(firstName) + } + + // Get last name + if adminLastName == "" { + fmt.Print("Enter admin last name: ") + lastName, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read last name: %w", err) + } + adminLastName = strings.TrimSpace(lastName) + } + + // Get password securely + if adminPassword == "" { + fmt.Print("Enter admin password: ") + passwordBytes, err := terminal.ReadPassword(int(syscall.Stdin)) + if err != nil { + return fmt.Errorf("failed to read password: %w", err) + } + adminPassword = string(passwordBytes) + fmt.Println() // Add newline after password input + + // Confirm password + fmt.Print("Confirm admin password: ") + confirmPasswordBytes, err := terminal.ReadPassword(int(syscall.Stdin)) + if err != nil { + return fmt.Errorf("failed to read password confirmation: %w", err) + } + confirmPassword := string(confirmPasswordBytes) + fmt.Println() // Add newline after password confirmation + + if adminPassword != confirmPassword { + return fmt.Errorf("passwords do not match") + } + } + + return nil +} + +func validateAdminInput() error { + if adminEmail == "" { + return fmt.Errorf("email is required") + } + + if adminFirstName == "" { + return fmt.Errorf("first name is required") + } + + if adminLastName == "" { + return fmt.Errorf("last name is required") + } + + if adminPassword == "" { + return fmt.Errorf("password is required") + } + + if len(adminPassword) < 8 { + return fmt.Errorf("password must be at least 8 characters long") + } + + // Basic email validation + if !strings.Contains(adminEmail, "@") || !strings.Contains(adminEmail, ".") { + return fmt.Errorf("invalid email format") + } + + return nil +} + +func createAdminAccount(userService services.UserService) error { + // Create admin request + adminRequest := services.CreateAdminRequest{ + FirstName: adminFirstName, + LastName: adminLastName, + Email: adminEmail, + Password: adminPassword, + } + + // Create admin account using the user service + adminUser, err := userService.CreateAdmin(adminRequest) + if err != nil { + return fmt.Errorf("failed to create admin account: %w", err) + } + + // Log success (adminUser contains the created user info) + _ = adminUser + + return nil +} + +func init() { + // Add flags to create-admin command + createAdminCmd.Flags().StringVarP(&adminEmail, "email", "e", "", "Admin email address") + createAdminCmd.Flags().StringVarP(&adminPassword, "password", "p", "", "Admin password") + createAdminCmd.Flags().StringVarP(&adminFirstName, "first-name", "f", "", "Admin first name") + createAdminCmd.Flags().StringVarP(&adminLastName, "last-name", "l", "", "Admin last name") + createAdminCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Interactive mode for entering admin details") + + // Add subcommands to admin command + adminCmd.AddCommand(createAdminCmd) + + // Add admin command to root + rootCmd.AddCommand(adminCmd) +} diff --git a/internal/services/interfaces.go b/internal/services/interfaces.go index 0464648..1d25c91 100644 --- a/internal/services/interfaces.go +++ b/internal/services/interfaces.go @@ -14,6 +14,7 @@ type UserService interface { Login(email, password string) (*models.User, string, error) // returns user and JWT token GetProfile(userID uint) (*models.User, error) UpdateProfile(userID uint, req UpdateProfileRequest) (*models.User, error) + CreateAdmin(req CreateAdminRequest) (*models.User, error) } // BookingService handles booking operations @@ -146,3 +147,10 @@ type UpdateScheduleRequest struct { MaxBookings *int `json:"max_bookings"` IsAvailable *bool `json:"is_available"` } + +type CreateAdminRequest struct { + FirstName string `json:"first_name" validate:"required,min=2,max=100"` + LastName string `json:"last_name" validate:"required,min=2,max=100"` + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=8"` +} diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 56becd0..da226ca 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -180,3 +180,48 @@ func (s *userService) UpdateProfile(userID uint, req UpdateProfileRequest) (*mod return user, nil } + +// CreateAdmin creates a new admin account with validation and password hashing +func (s *userService) CreateAdmin(req CreateAdminRequest) (*models.User, error) { + // Validate the request + if err := s.validator.Struct(req); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + // Additional password validation + if len(req.Password) < 8 { + return nil, errors.New("password must be at least 8 characters long") + } + + // Normalize email + req.Email = strings.ToLower(strings.TrimSpace(req.Email)) + + // Check if user already exists + existingUser, err := s.userRepo.GetByEmail(req.Email) + if err == nil && existingUser != nil { + return nil, fmt.Errorf("user with email %s already exists", req.Email) + } + + // Create new admin user + user := &models.User{ + FirstName: strings.TrimSpace(req.FirstName), + LastName: strings.TrimSpace(req.LastName), + Email: req.Email, + IsAdmin: true, // Set admin flag + } + + // Hash the password + if err := user.HashPassword(req.Password); err != nil { + return nil, fmt.Errorf("failed to hash password: %w", err) + } + + // Save user to database + if err := s.userRepo.Create(user); err != nil { + return nil, fmt.Errorf("failed to create admin user: %w", err) + } + + // Clear password hash from response for security + user.PasswordHash = "" + + return user, nil +}