Skip to content

Commit ec4ca52

Browse files
brtkwrclaude
andcommitted
fix: people get defaults to self, tolerates 403 on restricted sections
- `zeltapp people get` (no arg) and `zeltapp people get me` both fetch the current user - default sections changed to basic/about/work-contact/role (universally readable by managers); previous default included `personal` which is self-only and 403'd for any other user - per-section 403/404 is now skipped with a stderr note rather than aborting the whole command; --strict restores hard-fail behaviour - changelog config: group commits by type with emoji headers instead of dropping docs/chore commits entirely (which left v0.0.2 with an empty release body) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b2fa5e3 commit ec4ca52

3 files changed

Lines changed: 102 additions & 13 deletions

File tree

.goreleaser.yaml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,25 @@ brews:
4343
4444
changelog:
4545
sort: asc
46+
use: github
47+
groups:
48+
- title: "🚀 Features"
49+
regexp: '^.*?feat(\(.+\))??!?:.+$'
50+
order: 0
51+
- title: "🐛 Bug fixes"
52+
regexp: '^.*?fix(\(.+\))??!?:.+$'
53+
order: 1
54+
- title: "📚 Documentation"
55+
regexp: '^.*?docs(\(.+\))??!?:.+$'
56+
order: 2
57+
- title: "🛠 Maintenance"
58+
regexp: '^.*?(chore|build|refactor|style|perf)(\(.+\))??!?:.+$'
59+
order: 3
60+
- title: Other
61+
order: 999
4662
filters:
4763
exclude:
48-
- "^docs:"
4964
- "^test:"
50-
- "^chore:"
5165
- "^ci:"
66+
- "Merge pull request"
67+
- "Merge branch"

cmd/zeltapp/people.go

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,36 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"os"
78
"strconv"
89
"strings"
910

1011
"github.com/spf13/cobra"
1112
)
1213

14+
// fetchManyForUserTolerant is like fetchManyForUser but skips paths the user
15+
// can't read (403/404) instead of aborting. Returns the partial results,
16+
// the list of skipped sections, and a hard error only for other failures.
17+
func fetchManyForUserTolerant(c *client, userID int, strict bool, paths ...string) (map[string]json.RawMessage, []string, error) {
18+
out := map[string]json.RawMessage{}
19+
var skipped []string
20+
for _, p := range paths {
21+
var v json.RawMessage
22+
full := fmt.Sprintf("/apiv2/users/%d/%s", userID, p)
23+
err := c.do("GET", full, nil, &v)
24+
if err != nil {
25+
var ae *apiError
26+
if !strict && errors.As(err, &ae) && (ae.Status == 403 || ae.Status == 404) {
27+
skipped = append(skipped, p)
28+
continue
29+
}
30+
return nil, nil, err
31+
}
32+
out[p] = v
33+
}
34+
return out, skipped, nil
35+
}
36+
1337
// peopleCacheEntry is a tolerant view of /apiv2/users/cache rows. Zelt may
1438
// return more fields; we only project the ones useful for listing.
1539
type peopleCacheEntry struct {
@@ -122,29 +146,49 @@ func peopleSearchCmd() *cobra.Command {
122146

123147
func peopleGetCmd() *cobra.Command {
124148
var sections []string
149+
var strict bool
125150
cmd := &cobra.Command{
126-
Use: "get ID_OR_EMAIL",
127-
Short: "Get one person's profile by userId or email",
128-
Args: cobra.ExactArgs(1),
151+
Use: "get [ID_OR_EMAIL|me]",
152+
Short: "Get one person's profile by userId, email, or `me` (default: me)",
153+
Args: cobra.MaximumNArgs(1),
129154
RunE: func(cmd *cobra.Command, args []string) error {
130155
return withClient(func(c *client) error {
131-
id, err := resolveUserID(c, args[0])
132-
if err != nil {
133-
return err
156+
target := "me"
157+
if len(args) == 1 {
158+
target = args[0]
159+
}
160+
var id int
161+
if target == "me" || target == "self" {
162+
id = c.session.UserID
163+
} else {
164+
var err error
165+
id, err = resolveUserID(c, target)
166+
if err != nil {
167+
return err
168+
}
134169
}
135170
if len(sections) == 0 {
136-
sections = []string{"basic", "personal", "role", "work-contact"}
171+
// Default to sections you can typically read for any user:
172+
// basic, about, work-contact, role. Self has access to more
173+
// (personal, address, family, etc) — pass --sections explicitly
174+
// to request those.
175+
sections = []string{"basic", "about", "work-contact", "role"}
137176
}
138-
out, err := fetchManyForUser(c, id, sections...)
177+
out, skipped, err := fetchManyForUserTolerant(c, id, strict, sections...)
139178
if err != nil {
140179
return err
141180
}
181+
if len(skipped) > 0 {
182+
fmt.Fprintf(os.Stderr, "skipped (no access): %s\n", strings.Join(skipped, ", "))
183+
}
142184
return printResult(out)
143185
})
144186
},
145187
}
146188
cmd.Flags().StringSliceVar(&sections, "sections", nil,
147-
"comma-separated subpaths under /apiv2/users/<id>/ (default: basic,personal,role,work-contact)")
189+
"comma-separated subpaths under /apiv2/users/<id>/ (default: basic,about,work-contact,role)")
190+
cmd.Flags().BoolVar(&strict, "strict", false,
191+
"fail on the first 403/404 instead of skipping that section")
148192
return cmd
149193
}
150194

cmd/zeltapp/people_test.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,20 +115,49 @@ func TestCmd_PeopleSearch_NoMatches(t *testing.T) {
115115
func TestCmd_PeopleGetByID(t *testing.T) {
116116
srv, st, _, buf := withTestEnv(t)
117117
_ = srv
118-
for _, p := range []string{"basic", "personal", "role", "work-contact"} {
118+
for _, p := range []string{"basic", "about", "work-contact", "role"} {
119119
st.jsonRoute("GET", fmt.Sprintf("/apiv2/users/%d/%s", 1001, p), 200,
120120
mustJSON(map[string]any{"_section": p}))
121121
}
122122
if err := runCmd(t, "people", "get", "1001"); err != nil {
123123
t.Fatal(err)
124124
}
125-
for _, want := range []string{"basic", "personal", "role", "work-contact"} {
125+
for _, want := range []string{"basic", "about", "work-contact", "role"} {
126126
if !strings.Contains(buf.String(), want) {
127127
t.Errorf("expected %q in get output: %s", want, buf.String())
128128
}
129129
}
130130
}
131131

132+
func TestCmd_PeopleGet_SkipsForbiddenSections(t *testing.T) {
133+
srv, st, _, buf := withTestEnv(t)
134+
_ = srv
135+
st.jsonRoute("GET", "/apiv2/users/1001/basic", 200, mustJSON(map[string]any{"firstName": "X"}))
136+
st.jsonRoute("GET", "/apiv2/users/1001/about", 403, []byte(`{"message":"Forbidden"}`))
137+
st.jsonRoute("GET", "/apiv2/users/1001/work-contact", 200, mustJSON(map[string]any{"email": "x@y"}))
138+
st.jsonRoute("GET", "/apiv2/users/1001/role", 200, mustJSON(map[string]any{"title": "eng"}))
139+
if err := runCmd(t, "people", "get", "1001"); err != nil {
140+
t.Fatalf("expected success despite 403 on `about`: %v", err)
141+
}
142+
if !strings.Contains(buf.String(), "basic") {
143+
t.Errorf("expected basic in output: %s", buf.String())
144+
}
145+
if strings.Contains(buf.String(), `"about":`) {
146+
t.Errorf("about should be skipped (forbidden): %s", buf.String())
147+
}
148+
}
149+
150+
func TestCmd_PeopleGet_StrictModeFailsOnForbidden(t *testing.T) {
151+
srv, st, _, _ := withTestEnv(t)
152+
_ = srv
153+
st.jsonRoute("GET", "/apiv2/users/1001/basic", 200, []byte(`{}`))
154+
st.jsonRoute("GET", "/apiv2/users/1001/about", 403, []byte(`{"message":"Forbidden"}`))
155+
err := runCmd(t, "people", "get", "1001", "--strict")
156+
if err == nil {
157+
t.Fatal("expected error in strict mode on 403")
158+
}
159+
}
160+
132161
func TestCmd_PeopleGetByEmail(t *testing.T) {
133162
srv, st, _, _ := withTestEnv(t)
134163
_ = srv

0 commit comments

Comments
 (0)