Skip to content

Commit 743cb7c

Browse files
Add list users API endpoint and UI page
1 parent 03ce12a commit 743cb7c

11 files changed

Lines changed: 286 additions & 91 deletions

File tree

apiserver/controllers/users.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2022 Cloudbase Solutions SRL
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
// not use this file except in compliance with the License. You may obtain
5+
// a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
// License for the specific language governing permissions and limitations
13+
// under the License.
14+
15+
package controllers
16+
17+
import (
18+
"encoding/json"
19+
"log/slog"
20+
"net/http"
21+
)
22+
23+
// swagger:route GET /users users ListUsers
24+
//
25+
// List all users.
26+
//
27+
// Responses:
28+
// 200: Users
29+
// default: APIErrorResponse
30+
func (a *APIController) ListUsersHandler(w http.ResponseWriter, r *http.Request) {
31+
ctx := r.Context()
32+
33+
users, err := a.r.ListUsers(ctx)
34+
if err != nil {
35+
slog.With(slog.Any("error", err)).ErrorContext(ctx, "listing users")
36+
handleError(ctx, w, err)
37+
return
38+
}
39+
40+
w.Header().Set("Content-Type", "application/json")
41+
if err := json.NewEncoder(w).Encode(users); err != nil {
42+
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
43+
}
44+
}
45+

apiserver/routers/routers.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,13 @@ func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware
251251
apiRouter.Handle("/metrics-token/", http.HandlerFunc(han.MetricsTokenHandler)).Methods("GET", "OPTIONS")
252252
apiRouter.Handle("/metrics-token", http.HandlerFunc(han.MetricsTokenHandler)).Methods("GET", "OPTIONS")
253253

254+
///////////
255+
// Users //
256+
///////////
257+
// List users
258+
apiRouter.Handle("/users/", http.HandlerFunc(han.ListUsersHandler)).Methods("GET", "OPTIONS")
259+
apiRouter.Handle("/users", http.HandlerFunc(han.ListUsersHandler)).Methods("GET", "OPTIONS")
260+
254261
/////////////
255262
// Objects //
256263
/////////////

database/common/store.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ type UserStore interface {
8585
GetUser(ctx context.Context, user string) (params.User, error)
8686
GetUserByID(ctx context.Context, userID string) (params.User, error)
8787
GetAdminUser(ctx context.Context) (params.User, error)
88+
ListUsers(ctx context.Context) ([]params.User, error)
8889

8990
CreateUser(ctx context.Context, user params.NewUserParams) (params.User, error)
9091
UpdateUser(ctx context.Context, user string, param params.UpdateUserParams) (params.User, error)

database/sql/users.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,17 @@ func (s *sqlDatabase) GetAdminUser(_ context.Context) (params.User, error) {
163163
}
164164
return s.sqlToParamsUser(user), nil
165165
}
166+
167+
func (s *sqlDatabase) ListUsers(_ context.Context) ([]params.User, error) {
168+
var users []User
169+
q := s.conn.Model(&User{}).Find(&users)
170+
if q.Error != nil {
171+
return nil, fmt.Errorf("error fetching users: %w", q.Error)
172+
}
173+
174+
ret := make([]params.User, len(users))
175+
for idx, user := range users {
176+
ret[idx] = s.sqlToParamsUser(user)
177+
}
178+
return ret, nil
179+
}

runner/runner.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,3 +977,11 @@ func (r *Runner) getGHCliFromInstance(ctx context.Context, instance params.Insta
977977
}
978978
return ghCli, scaleSetCli, nil
979979
}
980+
981+
func (r *Runner) ListUsers(ctx context.Context) ([]params.User, error) {
982+
users, err := r.store.ListUsers(ctx)
983+
if err != nil {
984+
return nil, fmt.Errorf("error fetching users: %w", err)
985+
}
986+
return users, nil
987+
}

webapp/assets/assets.go

Lines changed: 0 additions & 83 deletions
This file was deleted.

webapp/assets/openapitools.json

Lines changed: 0 additions & 7 deletions
This file was deleted.

webapp/src/lib/api/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
type FileObject,
2828
type FileObjectPaginatedResponse,
2929
type UpdateFileObjectParams,
30+
type User,
3031
} from './generated-client.js';
3132

3233
// Import endpoint and credentials types directly
@@ -68,6 +69,7 @@ export type {
6869
FileObject,
6970
FileObjectPaginatedResponse,
7071
UpdateFileObjectParams,
72+
User,
7173
};
7274

7375
// Legacy APIError type for backward compatibility

webapp/src/lib/api/generated-client.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,27 @@ export class GeneratedGarmApiClient {
677677
async deleteFileObject(objectID: string): Promise<void> {
678678
await this.objectsApi.deleteFileObject(objectID);
679679
}
680+
681+
// User methods (not in generated API yet)
682+
async listUsers(): Promise<User[]> {
683+
const isDevMode = this.isDevelopmentMode();
684+
const headers: Record<string, string> = {
685+
'Content-Type': 'application/json',
686+
};
687+
if (this.token) {
688+
headers['Authorization'] = `Bearer ${this.token}`;
689+
}
690+
const response = await fetch(`${this.baseUrl}/api/v1/users`, {
691+
method: 'GET',
692+
headers,
693+
credentials: isDevMode ? 'omit' : 'include',
694+
});
695+
if (!response.ok) {
696+
const errorData = await response.json().catch(() => ({ error: response.statusText }));
697+
throw new Error(errorData.error || errorData.details || 'Failed to fetch users');
698+
}
699+
return response.json();
700+
}
680701
}
681702

682703
// Create a singleton instance

webapp/src/lib/components/Navigation.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@
5555
label: 'Organizations',
5656
icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z' // Users/group icon
5757
},
58+
{
59+
href: resolve('/users'),
60+
label: 'Users',
61+
icon: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z'
62+
},
5863
{
5964
href: resolve('/enterprises'),
6065
label: 'Enterprises',
@@ -384,4 +389,4 @@
384389
<!-- Close user menu when clicking outside -->
385390
{#if userMenuOpen}
386391
<div class="fixed inset-0 z-10" on:click={() => userMenuOpen = false} on:keydown={(e) => { if (e.key === 'Escape') userMenuOpen = false; }} role="button" tabindex="0" aria-label="Close user menu"></div>
387-
{/if}
392+
{/if}

0 commit comments

Comments
 (0)