diff --git a/cmd/hf/main.go b/cmd/hf/main.go index 3f25c50..7f20ce2 100644 --- a/cmd/hf/main.go +++ b/cmd/hf/main.go @@ -137,9 +137,104 @@ func handleGroup(group help.Group, args []string) { return } + // Dispatch implemented commands + remaining := args[1:] + switch group.Name { + case "user": + handleUserCommand(sub.Name, remaining) + return + } + output.Errorf("hf %s %s is recognized but not implemented yet", group.Name, sub.Name) } +func handleUserCommand(subCmd string, args []string) { + // Extract --token and --acc-mgr-token flags from args + tokenFlag := "" + accMgrTokenFlag := "" + var filtered []string + for i := 0; i < len(args); i++ { + switch args[i] { + case "--token": + if i+1 < len(args) { + i++ + tokenFlag = args[i] + } + case "--acc-mgr-token": + if i+1 < len(args) { + i++ + accMgrTokenFlag = args[i] + } + default: + filtered = append(filtered, args[i]) + } + } + + switch subCmd { + case "list": + commands.RunUserList(tokenFlag) + case "get": + if len(filtered) < 1 { + output.Error("usage: hf user get ") + } + commands.RunUserGet(filtered[0], tokenFlag) + case "create": + username, password, email, fullName := "", "", "", "" + for i := 0; i < len(filtered); i++ { + switch filtered[i] { + case "--user": + if i+1 < len(filtered) { + i++ + username = filtered[i] + } + case "--pass": + if i+1 < len(filtered) { + i++ + password = filtered[i] + } + case "--email": + if i+1 < len(filtered) { + i++ + email = filtered[i] + } + case "--full-name": + if i+1 < len(filtered) { + i++ + fullName = filtered[i] + } + default: + output.Errorf("unknown flag: %s", filtered[i]) + } + } + if username == "" { + output.Error("usage: hf user create --user ") + } + commands.RunUserCreate(username, password, email, fullName, accMgrTokenFlag) + case "update": + if len(filtered) < 1 { + output.Error("usage: hf user update [--email ...] [--full-name ...] [--pass ...] [--active ...]") + } + commands.RunUserUpdate(filtered[0], filtered[1:], tokenFlag) + case "activate": + if len(filtered) < 1 { + output.Error("usage: hf user activate ") + } + commands.RunUserActivate(filtered[0], tokenFlag) + case "deactivate": + if len(filtered) < 1 { + output.Error("usage: hf user deactivate ") + } + commands.RunUserDeactivate(filtered[0], tokenFlag) + case "delete": + if len(filtered) < 1 { + output.Error("usage: hf user delete ") + } + commands.RunUserDelete(filtered[0], tokenFlag) + default: + output.Errorf("hf user %s is not implemented yet", subCmd) + } +} + func isHelpFlagOnly(args []string) bool { return len(args) == 1 && (args[0] == "--help" || args[0] == "-h") } @@ -174,13 +269,13 @@ func topGroups() []help.Group { Description: "Manage users", Permitted: true, SubCommands: []help.Command{ - {Name: "create", Description: "Create a user account (uses account-manager token flow)", Permitted: false}, - {Name: "list", Description: "List users", Permitted: false}, - {Name: "get", Description: "Show a user by username", Permitted: false}, - {Name: "update", Description: "Update a user", Permitted: false}, - {Name: "activate", Description: "Activate a user", Permitted: false}, - {Name: "deactivate", Description: "Deactivate a user", Permitted: false}, - {Name: "delete", Description: "Delete a user", Permitted: false}, + {Name: "create", Description: "Create a user account (uses account-manager token flow)", Permitted: true}, + {Name: "list", Description: "List users", Permitted: true}, + {Name: "get", Description: "Show a user by username", Permitted: true}, + {Name: "update", Description: "Update a user", Permitted: true}, + {Name: "activate", Description: "Activate a user", Permitted: true}, + {Name: "deactivate", Description: "Deactivate a user", Permitted: true}, + {Name: "delete", Description: "Delete a user", Permitted: true}, }, }, { diff --git a/internal/commands/user.go b/internal/commands/user.go new file mode 100644 index 0000000..445b1ac --- /dev/null +++ b/internal/commands/user.go @@ -0,0 +1,331 @@ +package commands + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + "git.hangman-lab.top/zhi/HarborForge.Cli/internal/client" + "git.hangman-lab.top/zhi/HarborForge.Cli/internal/config" + "git.hangman-lab.top/zhi/HarborForge.Cli/internal/mode" + "git.hangman-lab.top/zhi/HarborForge.Cli/internal/output" + "git.hangman-lab.top/zhi/HarborForge.Cli/internal/passmgr" +) + +// userResponse matches the backend UserResponse schema. +type userResponse struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + FullName *string `json:"full_name"` + IsActive bool `json:"is_active"` + IsAdmin bool `json:"is_admin"` + RoleID *int `json:"role_id"` + RoleName *string `json:"role_name"` + CreatedAt string `json:"created_at"` +} + +// RunUserList implements `hf user list`. +func RunUserList(tokenFlag string) { + token := ResolveToken(tokenFlag) + cfg, err := config.Load() + if err != nil { + output.Errorf("config error: %v", err) + } + c := client.New(cfg.BaseURL, token) + data, err := c.Get("/users") + if err != nil { + output.Errorf("failed to list users: %v", err) + } + + if output.JSONMode { + var raw json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + output.Errorf("invalid JSON response: %v", err) + } + output.PrintJSON(raw) + return + } + + var users []userResponse + if err := json.Unmarshal(data, &users); err != nil { + output.Errorf("cannot parse user list: %v", err) + } + + headers := []string{"USERNAME", "EMAIL", "FULL NAME", "ROLE", "ACTIVE", "ADMIN"} + var rows [][]string + for _, u := range users { + fullName := "" + if u.FullName != nil { + fullName = *u.FullName + } + roleName := "" + if u.RoleName != nil { + roleName = *u.RoleName + } + active := "yes" + if !u.IsActive { + active = "no" + } + admin := "" + if u.IsAdmin { + admin = "yes" + } + rows = append(rows, []string{u.Username, u.Email, fullName, roleName, active, admin}) + } + output.PrintTable(headers, rows) +} + +// RunUserGet implements `hf user get `. +func RunUserGet(username, tokenFlag string) { + token := ResolveToken(tokenFlag) + cfg, err := config.Load() + if err != nil { + output.Errorf("config error: %v", err) + } + c := client.New(cfg.BaseURL, token) + data, err := c.Get("/users/" + username) + if err != nil { + output.Errorf("failed to get user: %v", err) + } + + if output.JSONMode { + var raw json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + output.Errorf("invalid JSON response: %v", err) + } + output.PrintJSON(raw) + return + } + + var u userResponse + if err := json.Unmarshal(data, &u); err != nil { + output.Errorf("cannot parse user: %v", err) + } + + fullName := "" + if u.FullName != nil { + fullName = *u.FullName + } + roleName := "" + if u.RoleName != nil { + roleName = *u.RoleName + } + active := "yes" + if !u.IsActive { + active = "no" + } + admin := "" + if u.IsAdmin { + admin = "yes" + } + output.PrintKeyValue( + "username", u.Username, + "email", u.Email, + "full-name", fullName, + "role", roleName, + "active", active, + "admin", admin, + "created", u.CreatedAt, + ) +} + +// userCreatePayload is the JSON body for POST /users. +type userCreatePayload struct { + Username string `json:"username"` + Email string `json:"email"` + FullName *string `json:"full_name,omitempty"` + Password *string `json:"password,omitempty"` +} + +// RunUserCreate implements `hf user create`. +func RunUserCreate(username, password, email, fullName, accMgrTokenFlag string) { + // Resolve account-manager token + var accMgrToken string + if mode.IsPaddedCell() { + if accMgrTokenFlag != "" { + output.Error("padded-cell installed, --acc-mgr-token flag disabled, use command directly") + } + tok, err := passmgr.GetAccountManagerToken() + if err != nil { + output.Error("--acc-mgr-token required or execute with pcexec") + } + accMgrToken = tok + } else { + if accMgrTokenFlag == "" { + output.Error("--acc-mgr-token required or execute with pcexec") + } + accMgrToken = accMgrTokenFlag + } + + // Resolve password + if password == "" && mode.IsPaddedCell() { + pw, err := passmgr.GeneratePassword("hf", username) + if err != nil { + output.Error("--pass required or execute with pcexec") + } + password = pw + } + if password == "" && !mode.IsPaddedCell() { + output.Error("--pass required or execute with pcexec") + } + + // Resolve email (default to username@harborforge.local if not provided) + if email == "" { + email = username + "@harborforge.local" + } + + payload := userCreatePayload{ + Username: username, + Email: email, + Password: &password, + } + if fullName != "" { + payload.FullName = &fullName + } + + body, err := json.Marshal(payload) + if err != nil { + output.Errorf("cannot marshal payload: %v", err) + } + + cfg, err := config.Load() + if err != nil { + output.Errorf("config error: %v", err) + } + c := client.New(cfg.BaseURL, accMgrToken) + data, err := c.Post("/users", bytes.NewReader(body)) + if err != nil { + output.Errorf("failed to create user: %v", err) + } + + if output.JSONMode { + var raw json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + output.Errorf("invalid JSON response: %v", err) + } + output.PrintJSON(raw) + return + } + + var u userResponse + if err := json.Unmarshal(data, &u); err != nil { + output.Errorf("cannot parse response: %v", err) + } + fmt.Printf("user created: %s\n", u.Username) +} + +// RunUserUpdate implements `hf user update `. +func RunUserUpdate(username string, args []string, tokenFlag string) { + token := ResolveToken(tokenFlag) + + payload := make(map[string]interface{}) + for i := 0; i < len(args); i++ { + switch args[i] { + case "--email": + if i+1 >= len(args) { + output.Error("--email requires a value") + } + i++ + payload["email"] = args[i] + case "--full-name": + if i+1 >= len(args) { + output.Error("--full-name requires a value") + } + i++ + payload["full_name"] = args[i] + case "--pass": + if i+1 >= len(args) { + output.Error("--pass requires a value") + } + i++ + payload["password"] = args[i] + case "--active": + if i+1 >= len(args) { + output.Error("--active requires true or false") + } + i++ + payload["is_active"] = strings.ToLower(args[i]) == "true" + default: + output.Errorf("unknown flag: %s", args[i]) + } + } + + if len(payload) == 0 { + output.Error("nothing to update — provide at least one flag") + } + + body, err := json.Marshal(payload) + if err != nil { + output.Errorf("cannot marshal payload: %v", err) + } + + cfg, err := config.Load() + if err != nil { + output.Errorf("config error: %v", err) + } + c := client.New(cfg.BaseURL, token) + data, err := c.Patch("/users/"+username, bytes.NewReader(body)) + if err != nil { + output.Errorf("failed to update user: %v", err) + } + + if output.JSONMode { + var raw json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + output.Errorf("invalid JSON response: %v", err) + } + output.PrintJSON(raw) + return + } + + fmt.Printf("user updated: %s\n", username) +} + +// RunUserActivate implements `hf user activate `. +func RunUserActivate(username, tokenFlag string) { + token := ResolveToken(tokenFlag) + cfg, err := config.Load() + if err != nil { + output.Errorf("config error: %v", err) + } + body, _ := json.Marshal(map[string]interface{}{"is_active": true}) + c := client.New(cfg.BaseURL, token) + _, err = c.Patch("/users/"+username, bytes.NewReader(body)) + if err != nil { + output.Errorf("failed to activate user: %v", err) + } + fmt.Printf("user activated: %s\n", username) +} + +// RunUserDeactivate implements `hf user deactivate `. +func RunUserDeactivate(username, tokenFlag string) { + token := ResolveToken(tokenFlag) + cfg, err := config.Load() + if err != nil { + output.Errorf("config error: %v", err) + } + body, _ := json.Marshal(map[string]interface{}{"is_active": false}) + c := client.New(cfg.BaseURL, token) + _, err = c.Patch("/users/"+username, bytes.NewReader(body)) + if err != nil { + output.Errorf("failed to deactivate user: %v", err) + } + fmt.Printf("user deactivated: %s\n", username) +} + +// RunUserDelete implements `hf user delete `. +func RunUserDelete(username, tokenFlag string) { + token := ResolveToken(tokenFlag) + cfg, err := config.Load() + if err != nil { + output.Errorf("config error: %v", err) + } + c := client.New(cfg.BaseURL, token) + _, err = c.Delete("/users/" + username) + if err != nil { + output.Errorf("failed to delete user: %v", err) + } + fmt.Printf("user deleted: %s\n", username) +}