Skip to content

Commit 5fa9e00

Browse files
fix: paginate application list and reuse existing API keys (#208)
1 parent b0cb38a commit 5fa9e00

4 files changed

Lines changed: 110 additions & 28 deletions

File tree

api/dashboard/client.go

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -204,39 +204,50 @@ func (c *Client) RevokeToken(token string) error {
204204
return nil
205205
}
206206

207-
// ListApplications returns all applications for the authenticated user.
207+
// ListApplications returns all applications for the authenticated user,
208+
// following pagination to fetch every page.
208209
func (c *Client) ListApplications(accessToken string) ([]Application, error) {
209-
req, err := http.NewRequest(http.MethodGet, c.APIURL+"/1/applications", nil)
210-
if err != nil {
211-
return nil, err
212-
}
213-
c.setAPIHeaders(req, accessToken)
210+
var allApps []Application
211+
page := 1
212+
213+
for {
214+
endpoint := fmt.Sprintf("%s/1/applications?page=%d", c.APIURL, page)
215+
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
216+
if err != nil {
217+
return nil, err
218+
}
219+
c.setAPIHeaders(req, accessToken)
214220

215-
resp, err := c.client.Do(req)
216-
if err != nil {
217-
return nil, fmt.Errorf("list applications request failed: %w", err)
218-
}
219-
defer resp.Body.Close()
221+
resp, err := c.client.Do(req)
222+
if err != nil {
223+
return nil, fmt.Errorf("list applications request failed: %w", err)
224+
}
225+
defer resp.Body.Close()
220226

221-
if resp.StatusCode == http.StatusUnauthorized {
222-
return nil, ErrSessionExpired
223-
}
227+
if resp.StatusCode == http.StatusUnauthorized {
228+
return nil, ErrSessionExpired
229+
}
224230

225-
if resp.StatusCode != http.StatusOK {
226-
return nil, fmt.Errorf("list applications failed with status: %d", resp.StatusCode)
227-
}
231+
if resp.StatusCode != http.StatusOK {
232+
return nil, fmt.Errorf("list applications failed with status: %d", resp.StatusCode)
233+
}
228234

229-
var appsResp ApplicationsResponse
230-
if err := json.NewDecoder(resp.Body).Decode(&appsResp); err != nil {
231-
return nil, fmt.Errorf("failed to parse applications response: %w", err)
232-
}
235+
var appsResp ApplicationsResponse
236+
if err := json.NewDecoder(resp.Body).Decode(&appsResp); err != nil {
237+
return nil, fmt.Errorf("failed to parse applications response: %w", err)
238+
}
233239

234-
apps := make([]Application, len(appsResp.Data))
235-
for i := range appsResp.Data {
236-
apps[i] = appsResp.Data[i].toApplication()
240+
for i := range appsResp.Data {
241+
allApps = append(allApps, appsResp.Data[i].toApplication())
242+
}
243+
244+
if appsResp.Meta.CurrentPage >= appsResp.Meta.TotalPages {
245+
break
246+
}
247+
page++
237248
}
238249

239-
return apps, nil
250+
return allApps, nil
240251
}
241252

242253
// GetApplication returns a single application by its ID.

api/dashboard/client_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,45 @@ func TestListApplications_Success(t *testing.T) {
133133
assert.Equal(t, "APP1", apps[0].ID)
134134
}
135135

136+
func TestListApplications_Paginated(t *testing.T) {
137+
mux := http.NewServeMux()
138+
callCount := 0
139+
140+
mux.HandleFunc("/1/applications", func(w http.ResponseWriter, r *http.Request) {
141+
callCount++
142+
page := r.URL.Query().Get("page")
143+
144+
switch page {
145+
case "2":
146+
require.NoError(t, json.NewEncoder(w).Encode(ApplicationsResponse{
147+
Data: []ApplicationResource{
148+
{ID: "APP3", Type: "application", Attributes: ApplicationAttributes{ApplicationID: "APP3", Name: "Third App"}},
149+
},
150+
Meta: PaginationMeta{TotalCount: 3, PerPage: 2, CurrentPage: 2, TotalPages: 2},
151+
}))
152+
default:
153+
require.NoError(t, json.NewEncoder(w).Encode(ApplicationsResponse{
154+
Data: []ApplicationResource{
155+
{ID: "APP1", Type: "application", Attributes: ApplicationAttributes{ApplicationID: "APP1", Name: "First App"}},
156+
{ID: "APP2", Type: "application", Attributes: ApplicationAttributes{ApplicationID: "APP2", Name: "Second App"}},
157+
},
158+
Meta: PaginationMeta{TotalCount: 3, PerPage: 2, CurrentPage: 1, TotalPages: 2},
159+
}))
160+
}
161+
})
162+
163+
ts, client := newTestClient(mux)
164+
defer ts.Close()
165+
166+
apps, err := client.ListApplications("test-token")
167+
require.NoError(t, err)
168+
assert.Len(t, apps, 3)
169+
assert.Equal(t, "APP1", apps[0].ID)
170+
assert.Equal(t, "APP2", apps[1].ID)
171+
assert.Equal(t, "APP3", apps[2].ID)
172+
assert.Equal(t, 2, callCount)
173+
}
174+
136175
func TestListApplications_Unauthorized(t *testing.T) {
137176
mux := http.NewServeMux()
138177
mux.HandleFunc("/1/applications", func(w http.ResponseWriter, r *http.Request) {

api/dashboard/types.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,27 @@ type Application struct {
4242
APIKey string `json:"api_key,omitempty"`
4343
}
4444

45+
// PaginationMeta contains page-based pagination metadata.
46+
type PaginationMeta struct {
47+
TotalCount int `json:"total_count"`
48+
PerPage int `json:"per_page"`
49+
CurrentPage int `json:"current_page"`
50+
TotalPages int `json:"total_pages"`
51+
}
52+
53+
// PaginationLinks contains pagination URLs.
54+
type PaginationLinks struct {
55+
First string `json:"first"`
56+
Last string `json:"last"`
57+
Prev string `json:"prev"`
58+
Next string `json:"next"`
59+
}
60+
4561
// ApplicationsResponse is the JSON:API response from GET /1/applications.
4662
type ApplicationsResponse struct {
47-
Data []ApplicationResource `json:"data"`
63+
Data []ApplicationResource `json:"data"`
64+
Meta PaginationMeta `json:"meta"`
65+
Links PaginationLinks `json:"links"`
4866
}
4967

5068
// SingleApplicationResponse is the JSON:API response from GET /1/application/:id.

pkg/cmd/auth/login/login.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,10 @@ func RunOAuthFlow(opts *LoginOptions, signup bool) error {
123123
}
124124

125125
appDetails = app
126-
if err := apputil.EnsureAPIKey(opts.IO, client, accessToken, appDetails); err != nil {
127-
return err
126+
if !reuseExistingAPIKey(opts.Config, appDetails) {
127+
if err := apputil.EnsureAPIKey(opts.IO, client, accessToken, appDetails); err != nil {
128+
return err
129+
}
128130
}
129131
}
130132

@@ -136,6 +138,18 @@ func RunOAuthFlow(opts *LoginOptions, signup bool) error {
136138
return apputil.ConfigureProfile(opts.IO, opts.Config, appDetails, profileName, opts.Default)
137139
}
138140

141+
// reuseExistingAPIKey checks if a local profile already has an API key for
142+
// the given application. If so, it sets app.APIKey and returns true.
143+
func reuseExistingAPIKey(cfg config.IConfig, app *dashboard.Application) bool {
144+
for _, p := range cfg.ConfiguredProfiles() {
145+
if p.ApplicationID == app.ID && p.APIKey != "" {
146+
app.APIKey = p.APIKey
147+
return true
148+
}
149+
}
150+
return false
151+
}
152+
139153
func selectApplication(opts *LoginOptions, apps []dashboard.Application, interactive bool) (*dashboard.Application, error) {
140154
if opts.AppName != "" {
141155
for i := range apps {

0 commit comments

Comments
 (0)