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/output" ) // roleResponse matches the backend role list schema. type roleResponse struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` IsGlobal bool `json:"is_global"` PermissionIDs []int `json:"permission_ids"` CreatedAt string `json:"created_at"` } // roleDetailResponse matches the backend role detail schema. type roleDetailResponse struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` IsGlobal bool `json:"is_global"` Permissions []permissionResponse `json:"permissions"` CreatedAt string `json:"created_at"` } // permissionResponse matches the backend permission schema. type permissionResponse struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` Category string `json:"category"` } func loadRoleClient(tokenFlag string) (*client.Client, error) { token := ResolveToken(tokenFlag) cfg, err := config.Load() if err != nil { return nil, fmt.Errorf("config error: %w", err) } return client.New(cfg.BaseURL, token), nil } func fetchRoles(c *client.Client) ([]roleResponse, error) { data, err := c.Get("/roles") if err != nil { return nil, err } var roles []roleResponse if err := json.Unmarshal(data, &roles); err != nil { return nil, fmt.Errorf("cannot parse role list: %w", err) } return roles, nil } func fetchPermissions(c *client.Client) ([]permissionResponse, error) { data, err := c.Get("/roles/permissions") if err != nil { return nil, err } var perms []permissionResponse if err := json.Unmarshal(data, &perms); err != nil { return nil, fmt.Errorf("cannot parse permission list: %w", err) } return perms, nil } func findRoleByName(c *client.Client, roleName string) (*roleResponse, error) { roles, err := fetchRoles(c) if err != nil { return nil, err } for _, r := range roles { if r.Name == roleName { role := r return &role, nil } } return nil, fmt.Errorf("role not found: %s", roleName) } func fetchRoleDetailByName(c *client.Client, roleName string) (*roleDetailResponse, error) { role, err := findRoleByName(c, roleName) if err != nil { return nil, err } data, err := c.Get(fmt.Sprintf("/roles/%d", role.ID)) if err != nil { return nil, err } var detail roleDetailResponse if err := json.Unmarshal(data, &detail); err != nil { return nil, fmt.Errorf("cannot parse role detail: %w", err) } return &detail, nil } func resolvePermissionIDs(c *client.Client, names []string) ([]int, error) { perms, err := fetchPermissions(c) if err != nil { return nil, err } byName := make(map[string]int, len(perms)) for _, p := range perms { byName[p.Name] = p.ID } ids := make([]int, 0, len(names)) seen := map[int]struct{}{} var missing []string for _, name := range names { id, ok := byName[name] if !ok { missing = append(missing, name) continue } if _, exists := seen[id]; exists { continue } seen[id] = struct{}{} ids = append(ids, id) } if len(missing) > 0 { return nil, fmt.Errorf("unknown permission(s): %s", strings.Join(missing, ", ")) } return ids, nil } func replaceRolePermissions(c *client.Client, roleID int, permissionIDs []int) error { payload := map[string]interface{}{ "permission_ids": permissionIDs, } body, err := json.Marshal(payload) if err != nil { return fmt.Errorf("cannot marshal payload: %w", err) } _, err = c.Post(fmt.Sprintf("/roles/%d/permissions", roleID), bytes.NewReader(body)) return err } // RunRoleList implements `hf role list`. func RunRoleList(tokenFlag string) { c, err := loadRoleClient(tokenFlag) if err != nil { output.Errorf("%v", err) } data, err := c.Get("/roles") if err != nil { output.Errorf("failed to list roles: %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 roles []roleResponse if err := json.Unmarshal(data, &roles); err != nil { output.Errorf("cannot parse role list: %v", err) } headers := []string{"NAME", "DESCRIPTION", "GLOBAL", "PERM IDS"} var rows [][]string for _, r := range roles { global := "" if r.IsGlobal { global = "yes" } permIDs := "" if len(r.PermissionIDs) > 0 { parts := make([]string, 0, len(r.PermissionIDs)) for _, id := range r.PermissionIDs { parts = append(parts, fmt.Sprintf("%d", id)) } permIDs = strings.Join(parts, ", ") } rows = append(rows, []string{r.Name, r.Description, global, permIDs}) } output.PrintTable(headers, rows) } // RunRoleGet implements `hf role get `. func RunRoleGet(roleName, tokenFlag string) { c, err := loadRoleClient(tokenFlag) if err != nil { output.Errorf("%v", err) } role, err := findRoleByName(c, roleName) if err != nil { output.Errorf("failed to get role: %v", err) } data, err := c.Get(fmt.Sprintf("/roles/%d", role.ID)) if err != nil { output.Errorf("failed to get role: %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 r roleDetailResponse if err := json.Unmarshal(data, &r); err != nil { output.Errorf("cannot parse role: %v", err) } global := "no" if r.IsGlobal { global = "yes" } perms := "(none)" if len(r.Permissions) > 0 { names := make([]string, 0, len(r.Permissions)) for _, p := range r.Permissions { names = append(names, p.Name) } perms = strings.Join(names, ", ") } output.PrintKeyValue( "name", r.Name, "description", r.Description, "global", global, "permissions", perms, ) } // RunRoleCreate implements `hf role create`. func RunRoleCreate(name, desc string, global bool, tokenFlag string) { c, err := loadRoleClient(tokenFlag) if err != nil { output.Errorf("%v", err) } if name == "" { output.Error("usage: hf role create --name ") } payload := map[string]interface{}{ "name": name, } if desc != "" { payload["description"] = desc } if global { payload["is_global"] = true } body, err := json.Marshal(payload) if err != nil { output.Errorf("cannot marshal payload: %v", err) } data, err := c.Post("/roles", bytes.NewReader(body)) if err != nil { output.Errorf("failed to create role: %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("role created: %s\n", name) } // RunRoleUpdate implements `hf role update `. func RunRoleUpdate(roleName string, args []string, tokenFlag string) { c, err := loadRoleClient(tokenFlag) if err != nil { output.Errorf("%v", err) } role, err := findRoleByName(c, roleName) if err != nil { output.Errorf("failed to update role: %v", err) } payload := make(map[string]interface{}) for i := 0; i < len(args); i++ { switch args[i] { case "--desc": if i+1 >= len(args) { output.Error("--desc requires a value") } i++ payload["description"] = args[i] 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) } _, err = c.Patch(fmt.Sprintf("/roles/%d", role.ID), bytes.NewReader(body)) if err != nil { output.Errorf("failed to update role: %v", err) } fmt.Printf("role updated: %s\n", roleName) } // RunRoleDelete implements `hf role delete `. func RunRoleDelete(roleName, tokenFlag string) { c, err := loadRoleClient(tokenFlag) if err != nil { output.Errorf("%v", err) } role, err := findRoleByName(c, roleName) if err != nil { output.Errorf("failed to delete role: %v", err) } _, err = c.Delete(fmt.Sprintf("/roles/%d", role.ID)) if err != nil { output.Errorf("failed to delete role: %v", err) } fmt.Printf("role deleted: %s\n", roleName) } // RunRoleSetPermissions implements `hf role set-permissions --permission [...]`. func RunRoleSetPermissions(roleName string, permissions []string, tokenFlag string) { c, err := loadRoleClient(tokenFlag) if err != nil { output.Errorf("%v", err) } if len(permissions) == 0 { output.Error("usage: hf role set-permissions --permission [--permission ...]") } role, err := findRoleByName(c, roleName) if err != nil { output.Errorf("failed to set permissions: %v", err) } permissionIDs, err := resolvePermissionIDs(c, permissions) if err != nil { output.Errorf("failed to set permissions: %v", err) } if err := replaceRolePermissions(c, role.ID, permissionIDs); err != nil { output.Errorf("failed to set permissions: %v", err) } fmt.Printf("permissions set for role %s: %s\n", roleName, strings.Join(permissions, ", ")) } // RunRoleAddPermissions implements `hf role add-permissions --permission [...]`. func RunRoleAddPermissions(roleName string, permissions []string, tokenFlag string) { c, err := loadRoleClient(tokenFlag) if err != nil { output.Errorf("%v", err) } if len(permissions) == 0 { output.Error("usage: hf role add-permissions --permission [--permission ...]") } role, err := findRoleByName(c, roleName) if err != nil { output.Errorf("failed to add permissions: %v", err) } detail, err := fetchRoleDetailByName(c, roleName) if err != nil { output.Errorf("failed to add permissions: %v", err) } currentIDs := make([]int, 0, len(detail.Permissions)) seen := map[int]struct{}{} for _, p := range detail.Permissions { seen[p.ID] = struct{}{} currentIDs = append(currentIDs, p.ID) } newIDs, err := resolvePermissionIDs(c, permissions) if err != nil { output.Errorf("failed to add permissions: %v", err) } for _, id := range newIDs { if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} currentIDs = append(currentIDs, id) } if err := replaceRolePermissions(c, role.ID, currentIDs); err != nil { output.Errorf("failed to add permissions: %v", err) } fmt.Printf("permissions added to role %s: %s\n", roleName, strings.Join(permissions, ", ")) } // RunRoleRemovePermissions implements `hf role remove-permissions --permission [...]`. func RunRoleRemovePermissions(roleName string, permissions []string, tokenFlag string) { c, err := loadRoleClient(tokenFlag) if err != nil { output.Errorf("%v", err) } if len(permissions) == 0 { output.Error("usage: hf role remove-permissions --permission [--permission ...]") } role, err := findRoleByName(c, roleName) if err != nil { output.Errorf("failed to remove permissions: %v", err) } detail, err := fetchRoleDetailByName(c, roleName) if err != nil { output.Errorf("failed to remove permissions: %v", err) } removeIDs, err := resolvePermissionIDs(c, permissions) if err != nil { output.Errorf("failed to remove permissions: %v", err) } removeSet := map[int]struct{}{} for _, id := range removeIDs { removeSet[id] = struct{}{} } remaining := make([]int, 0, len(detail.Permissions)) for _, p := range detail.Permissions { if _, ok := removeSet[p.ID]; ok { continue } remaining = append(remaining, p.ID) } if err := replaceRolePermissions(c, role.ID, remaining); err != nil { output.Errorf("failed to remove permissions: %v", err) } fmt.Printf("permissions removed from role %s: %s\n", roleName, strings.Join(permissions, ", ")) } // RunPermissionList implements `hf permission list`. func RunPermissionList(tokenFlag string) { c, err := loadRoleClient(tokenFlag) if err != nil { output.Errorf("%v", err) } data, err := c.Get("/roles/permissions") if err != nil { output.Errorf("failed to list permissions: %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 perms []permissionResponse if err := json.Unmarshal(data, &perms); err != nil { output.Errorf("cannot parse permission list: %v", err) } headers := []string{"ID", "NAME", "CATEGORY", "DESCRIPTION"} var rows [][]string for _, p := range perms { rows = append(rows, []string{fmt.Sprintf("%d", p.ID), p.Name, p.Category, p.Description}) } output.PrintTable(headers, rows) }