Skip to content

Commit 69e26c4

Browse files
authored
feat: Allow using username in user queries (coder#1221)
* feat: Allow using username in user queries * Test needs a username/email to not match empty string
1 parent 365c96c commit 69e26c4

File tree

4 files changed

+95
-22
lines changed

4 files changed

+95
-22
lines changed

coderd/httpmw/userparam.go

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ import (
1414

1515
type userParamContextKey struct{}
1616

17+
const (
18+
// userErrorMessage is a constant so that no information about the state
19+
// of the queried user can be gained. We return the same error if the user
20+
// does not exist, or if the input is just garbage.
21+
userErrorMessage = "\"user\" must be an existing uuid or username"
22+
)
23+
1724
// UserParam returns the user from the ExtractUserParam handler.
1825
func UserParam(r *http.Request) database.User {
1926
user, ok := r.Context().Value(userParamContextKey{}).(database.User)
@@ -27,32 +34,56 @@ func UserParam(r *http.Request) database.User {
2734
func ExtractUserParam(db database.Store) func(http.Handler) http.Handler {
2835
return func(next http.Handler) http.Handler {
2936
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
30-
var userID uuid.UUID
31-
if chi.URLParam(r, "user") == "me" {
32-
userID = APIKey(r).UserID
37+
var user database.User
38+
var err error
39+
40+
// userQuery is either a uuid, a username, or 'me'
41+
userQuery := chi.URLParam(r, "user")
42+
if userQuery == "" {
43+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
44+
Message: "\"user\" must be provided",
45+
})
46+
return
47+
}
48+
49+
if userQuery == "me" {
50+
user, err = db.GetUserByID(r.Context(), APIKey(r).UserID)
51+
if err != nil {
52+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
53+
Message: fmt.Sprintf("get user: %s", err.Error()),
54+
})
55+
return
56+
}
57+
} else if userID, err := uuid.Parse(userQuery); err == nil {
58+
// If the userQuery is a valid uuid
59+
user, err = db.GetUserByID(r.Context(), userID)
60+
if err != nil {
61+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
62+
Message: userErrorMessage,
63+
})
64+
return
65+
}
3366
} else {
34-
var ok bool
35-
userID, ok = parseUUID(rw, r, "user")
36-
if !ok {
67+
// Try as a username last
68+
user, err = db.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
69+
Username: userQuery,
70+
})
71+
if err != nil {
72+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
73+
Message: userErrorMessage,
74+
})
3775
return
3876
}
3977
}
4078

4179
apiKey := APIKey(r)
42-
if apiKey.UserID != userID {
80+
if apiKey.UserID != user.ID {
4381
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
4482
Message: "getting non-personal users isn't supported yet",
4583
})
4684
return
4785
}
4886

49-
user, err := db.GetUserByID(r.Context(), userID)
50-
if err != nil {
51-
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
52-
Message: fmt.Sprintf("get user: %s", err.Error()),
53-
})
54-
}
55-
5687
ctx := context.WithValue(r.Context(), userParamContextKey{}, user)
5788
next.ServeHTTP(rw, r.WithContext(ctx))
5889
})

coderd/httpmw/userparam_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ func TestUserParam(t *testing.T) {
3434
})
3535

3636
user, err := db.InsertUser(r.Context(), database.InsertUserParams{
37-
ID: uuid.New(),
37+
ID: uuid.New(),
38+
Email: "admin@email.com",
39+
Username: "admin",
3840
})
3941
require.NoError(t, err)
4042

coderd/users_test.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -431,14 +431,45 @@ func TestPutUserSuspend(t *testing.T) {
431431
})
432432
}
433433

434-
func TestUserByName(t *testing.T) {
434+
func TestGetUser(t *testing.T) {
435435
t.Parallel()
436-
client := coderdtest.New(t, nil)
437-
firstUser := coderdtest.CreateFirstUser(t, client)
438-
user, err := client.User(context.Background(), codersdk.Me)
439436

440-
require.NoError(t, err)
441-
require.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
437+
t.Run("ByMe", func(t *testing.T) {
438+
t.Parallel()
439+
440+
client := coderdtest.New(t, nil)
441+
firstUser := coderdtest.CreateFirstUser(t, client)
442+
443+
user, err := client.User(context.Background(), codersdk.Me)
444+
require.NoError(t, err)
445+
require.Equal(t, firstUser.UserID, user.ID)
446+
require.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
447+
})
448+
449+
t.Run("ByID", func(t *testing.T) {
450+
t.Parallel()
451+
452+
client := coderdtest.New(t, nil)
453+
firstUser := coderdtest.CreateFirstUser(t, client)
454+
455+
user, err := client.User(context.Background(), firstUser.UserID)
456+
require.NoError(t, err)
457+
require.Equal(t, firstUser.UserID, user.ID)
458+
require.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
459+
})
460+
461+
t.Run("ByUsername", func(t *testing.T) {
462+
t.Parallel()
463+
464+
client := coderdtest.New(t, nil)
465+
firstUser := coderdtest.CreateFirstUser(t, client)
466+
exp, err := client.User(context.Background(), firstUser.UserID)
467+
require.NoError(t, err)
468+
469+
user, err := client.UserByUsername(context.Background(), exp.Username)
470+
require.NoError(t, err)
471+
require.Equal(t, exp, user)
472+
})
442473
}
443474

444475
func TestGetUsers(t *testing.T) {

codersdk/users.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,16 @@ func (c *Client) Logout(ctx context.Context) error {
274274
// User returns a user for the ID provided.
275275
// If the uuid is nil, the current user will be returned.
276276
func (c *Client) User(ctx context.Context, id uuid.UUID) (User, error) {
277-
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", uuidOrMe(id)), nil)
277+
return c.userByIdentifier(ctx, uuidOrMe(id))
278+
}
279+
280+
// UserByUsername returns a user for the username provided.
281+
func (c *Client) UserByUsername(ctx context.Context, username string) (User, error) {
282+
return c.userByIdentifier(ctx, username)
283+
}
284+
285+
func (c *Client) userByIdentifier(ctx context.Context, ident string) (User, error) {
286+
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", ident), nil)
278287
if err != nil {
279288
return User{}, err
280289
}

0 commit comments

Comments
 (0)