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:
parent
c265e8f866
commit
df39550eb1
3
go.mod
3
go.mod
@ -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
4
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.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
212
internal/cli/admin.go
Normal 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)
|
||||||
|
}
|
||||||
@ -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"`
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user