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.
This commit is contained in:
ats-tech25 2025-11-06 09:13:26 +00:00
parent c265e8f866
commit df39550eb1
5 changed files with 271 additions and 1 deletions

3
go.mod
View File

@ -47,7 +47,8 @@ require (
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/net v0.21.0 // indirect golang.org/x/net v0.21.0 // indirect
golang.org/x/sync v0.10.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 golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

4
go.sum
View File

@ -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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 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.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.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.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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=

212
internal/cli/admin.go Normal file
View File

@ -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)
}

View File

@ -14,6 +14,7 @@ type UserService interface {
Login(email, password string) (*models.User, string, error) // returns user and JWT token Login(email, password string) (*models.User, string, error) // returns user and JWT token
GetProfile(userID uint) (*models.User, error) GetProfile(userID uint) (*models.User, error)
UpdateProfile(userID uint, req UpdateProfileRequest) (*models.User, error) UpdateProfile(userID uint, req UpdateProfileRequest) (*models.User, error)
CreateAdmin(req CreateAdminRequest) (*models.User, error)
} }
// BookingService handles booking operations // BookingService handles booking operations
@ -146,3 +147,10 @@ type UpdateScheduleRequest struct {
MaxBookings *int `json:"max_bookings"` MaxBookings *int `json:"max_bookings"`
IsAvailable *bool `json:"is_available"` 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"`
}

View File

@ -180,3 +180,48 @@ func (s *userService) UpdateProfile(userID uint, req UpdateProfileRequest) (*mod
return user, nil 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
}