Merge dev-2026-03-21 into main #1
109
cmd/hf/main.go
109
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 <username>")
|
||||
}
|
||||
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 <username>")
|
||||
}
|
||||
commands.RunUserCreate(username, password, email, fullName, accMgrTokenFlag)
|
||||
case "update":
|
||||
if len(filtered) < 1 {
|
||||
output.Error("usage: hf user update <username> [--email ...] [--full-name ...] [--pass ...] [--active ...]")
|
||||
}
|
||||
commands.RunUserUpdate(filtered[0], filtered[1:], tokenFlag)
|
||||
case "activate":
|
||||
if len(filtered) < 1 {
|
||||
output.Error("usage: hf user activate <username>")
|
||||
}
|
||||
commands.RunUserActivate(filtered[0], tokenFlag)
|
||||
case "deactivate":
|
||||
if len(filtered) < 1 {
|
||||
output.Error("usage: hf user deactivate <username>")
|
||||
}
|
||||
commands.RunUserDeactivate(filtered[0], tokenFlag)
|
||||
case "delete":
|
||||
if len(filtered) < 1 {
|
||||
output.Error("usage: hf user delete <username>")
|
||||
}
|
||||
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},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
331
internal/commands/user.go
Normal file
331
internal/commands/user.go
Normal file
@@ -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 <username>`.
|
||||
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 <token> required or execute with pcexec")
|
||||
}
|
||||
accMgrToken = tok
|
||||
} else {
|
||||
if accMgrTokenFlag == "" {
|
||||
output.Error("--acc-mgr-token <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 <password> required or execute with pcexec")
|
||||
}
|
||||
password = pw
|
||||
}
|
||||
if password == "" && !mode.IsPaddedCell() {
|
||||
output.Error("--pass <password> 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 <username>`.
|
||||
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 <username>`.
|
||||
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 <username>`.
|
||||
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 <username>`.
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user