Skip to content

Commit 63d1465

Browse files
feat: Add update profile endpoint (#916)
1 parent db9d5b7 commit 63d1465

File tree

8 files changed

+264
-0
lines changed

8 files changed

+264
-0
lines changed

coderd/coderd.go

+1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ func New(options *Options) (http.Handler, func()) {
150150
r.Route("/{user}", func(r chi.Router) {
151151
r.Use(httpmw.ExtractUserParam(options.Database))
152152
r.Get("/", api.userByName)
153+
r.Put("/profile", api.putUserProfile)
153154
r.Get("/organizations", api.organizationsByUser)
154155
r.Post("/organizations", api.postOrganizationsByUser)
155156
r.Post("/keys", api.postAPIKey)

coderd/database/databasefake/databasefake.go

+17
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,23 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
10501050
return user, nil
10511051
}
10521052

1053+
func (q *fakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
1054+
q.mutex.Lock()
1055+
defer q.mutex.Unlock()
1056+
1057+
for index, user := range q.users {
1058+
if user.ID != arg.ID {
1059+
continue
1060+
}
1061+
user.Name = arg.Name
1062+
user.Email = arg.Email
1063+
user.Username = arg.Username
1064+
q.users[index] = user
1065+
return user, nil
1066+
}
1067+
return database.User{}, sql.ErrNoRows
1068+
}
1069+
10531070
func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWorkspaceParams) (database.Workspace, error) {
10541071
q.mutex.Lock()
10551072
defer q.mutex.Unlock()

coderd/database/querier.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

+43
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/users.sql

+11
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,14 @@ INSERT INTO
4040
)
4141
VALUES
4242
($1, $2, $3, $4, FALSE, $5, $6, $7, $8) RETURNING *;
43+
44+
-- name: UpdateUserProfile :one
45+
UPDATE
46+
users
47+
SET
48+
email = $2,
49+
"name" = $3,
50+
username = $4,
51+
updated_at = $5
52+
WHERE
53+
id = $1 RETURNING *;

coderd/users.go

+65
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,70 @@ func (*api) userByName(rw http.ResponseWriter, r *http.Request) {
270270
render.JSON(rw, r, convertUser(user))
271271
}
272272

273+
func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) {
274+
user := httpmw.UserParam(r)
275+
276+
var params codersdk.UpdateUserProfileRequest
277+
if !httpapi.Read(rw, r, &params) {
278+
return
279+
}
280+
281+
if params.Name == nil {
282+
params.Name = &user.Name
283+
}
284+
285+
existentUser, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
286+
Email: params.Email,
287+
Username: params.Username,
288+
})
289+
isDifferentUser := existentUser.ID != user.ID
290+
291+
if err == nil && isDifferentUser {
292+
responseErrors := []httpapi.Error{}
293+
if existentUser.Email == params.Email {
294+
responseErrors = append(responseErrors, httpapi.Error{
295+
Field: "email",
296+
Code: "exists",
297+
})
298+
}
299+
if existentUser.Username == params.Username {
300+
responseErrors = append(responseErrors, httpapi.Error{
301+
Field: "username",
302+
Code: "exists",
303+
})
304+
}
305+
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
306+
Message: fmt.Sprintf("user already exists"),
307+
Errors: responseErrors,
308+
})
309+
return
310+
}
311+
if !errors.Is(err, sql.ErrNoRows) && isDifferentUser {
312+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
313+
Message: fmt.Sprintf("get user: %s", err),
314+
})
315+
return
316+
}
317+
318+
updatedUserProfile, err := api.Database.UpdateUserProfile(r.Context(), database.UpdateUserProfileParams{
319+
ID: user.ID,
320+
Name: *params.Name,
321+
Email: params.Email,
322+
Username: params.Username,
323+
UpdatedAt: database.Now(),
324+
})
325+
326+
if err != nil {
327+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
328+
Message: fmt.Sprintf("patch user: %s", err.Error()),
329+
})
330+
return
331+
}
332+
333+
render.Status(r, http.StatusOK)
334+
render.JSON(rw, r, convertUser(updatedUserProfile))
335+
}
336+
273337
// Returns organizations the parameterized user has access to.
274338
func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
275339
user := httpmw.UserParam(r)
@@ -872,5 +936,6 @@ func convertUser(user database.User) codersdk.User {
872936
Email: user.Email,
873937
CreatedAt: user.CreatedAt,
874938
Username: user.Username,
939+
Name: user.Name,
875940
}
876941
}

coderd/users_test.go

+105
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,111 @@ func TestPostUsers(t *testing.T) {
200200
})
201201
}
202202

203+
func TestUpdateUserProfile(t *testing.T) {
204+
t.Parallel()
205+
t.Run("UserNotFound", func(t *testing.T) {
206+
t.Parallel()
207+
client := coderdtest.New(t, nil)
208+
coderdtest.CreateFirstUser(t, client)
209+
_, err := client.UpdateUserProfile(context.Background(), uuid.New(), codersdk.UpdateUserProfileRequest{
210+
Username: "newusername",
211+
Email: "newemail@coder.com",
212+
})
213+
var apiErr *codersdk.Error
214+
require.ErrorAs(t, err, &apiErr)
215+
// Right now, we are raising a BAD request error because we don't support a
216+
// user accessing other users info
217+
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
218+
})
219+
220+
t.Run("ConflictingEmail", func(t *testing.T) {
221+
t.Parallel()
222+
client := coderdtest.New(t, nil)
223+
user := coderdtest.CreateFirstUser(t, client)
224+
existentUser, _ := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
225+
Email: "bruno@coder.com",
226+
Username: "bruno",
227+
Password: "password",
228+
OrganizationID: user.OrganizationID,
229+
})
230+
_, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
231+
Username: "newusername",
232+
Email: existentUser.Email,
233+
})
234+
var apiErr *codersdk.Error
235+
require.ErrorAs(t, err, &apiErr)
236+
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
237+
})
238+
239+
t.Run("ConflictingUsername", func(t *testing.T) {
240+
t.Parallel()
241+
client := coderdtest.New(t, nil)
242+
user := coderdtest.CreateFirstUser(t, client)
243+
existentUser, _ := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
244+
Email: "bruno@coder.com",
245+
Username: "bruno",
246+
Password: "password",
247+
OrganizationID: user.OrganizationID,
248+
})
249+
_, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
250+
Username: existentUser.Username,
251+
Email: "newemail@coder.com",
252+
})
253+
var apiErr *codersdk.Error
254+
require.ErrorAs(t, err, &apiErr)
255+
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
256+
})
257+
258+
t.Run("UpdateUsernameAndEmail", func(t *testing.T) {
259+
t.Parallel()
260+
client := coderdtest.New(t, nil)
261+
coderdtest.CreateFirstUser(t, client)
262+
userProfile, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
263+
Username: "newusername",
264+
Email: "newemail@coder.com",
265+
})
266+
require.NoError(t, err)
267+
require.Equal(t, userProfile.Username, "newusername")
268+
require.Equal(t, userProfile.Email, "newemail@coder.com")
269+
})
270+
271+
t.Run("UpdateUsername", func(t *testing.T) {
272+
t.Parallel()
273+
client := coderdtest.New(t, nil)
274+
coderdtest.CreateFirstUser(t, client)
275+
me, _ := client.User(context.Background(), codersdk.Me)
276+
userProfile, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
277+
Username: me.Username,
278+
Email: "newemail@coder.com",
279+
})
280+
require.NoError(t, err)
281+
require.Equal(t, userProfile.Username, me.Username)
282+
require.Equal(t, userProfile.Email, "newemail@coder.com")
283+
})
284+
285+
t.Run("KeepUserName", func(t *testing.T) {
286+
t.Parallel()
287+
client := coderdtest.New(t, nil)
288+
coderdtest.CreateFirstUser(t, client)
289+
me, _ := client.User(context.Background(), codersdk.Me)
290+
newName := "New Name"
291+
firstProfile, _ := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
292+
Username: me.Username,
293+
Email: me.Email,
294+
Name: &newName,
295+
})
296+
t.Log(firstProfile)
297+
userProfile, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
298+
Username: "newusername",
299+
Email: "newemail@coder.com",
300+
})
301+
require.NoError(t, err)
302+
require.Equal(t, userProfile.Username, "newusername")
303+
require.Equal(t, userProfile.Email, "newemail@coder.com")
304+
require.Equal(t, userProfile.Name, newName)
305+
})
306+
}
307+
203308
func TestUserByName(t *testing.T) {
204309
t.Parallel()
205310
client := coderdtest.New(t, nil)

codersdk/users.go

+21
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type User struct {
1919
Email string `json:"email" validate:"required"`
2020
CreatedAt time.Time `json:"created_at" validate:"required"`
2121
Username string `json:"username" validate:"required"`
22+
Name string `json:"name"`
2223
}
2324

2425
type CreateFirstUserRequest struct {
@@ -41,6 +42,12 @@ type CreateUserRequest struct {
4142
OrganizationID uuid.UUID `json:"organization_id" validate:"required"`
4243
}
4344

45+
type UpdateUserProfileRequest struct {
46+
Email string `json:"email" validate:"required,email"`
47+
Username string `json:"username" validate:"required,username"`
48+
Name *string `json:"name"`
49+
}
50+
4451
// LoginWithPasswordRequest enables callers to authenticate with email and password.
4552
type LoginWithPasswordRequest struct {
4653
Email string `json:"email" validate:"required,email"`
@@ -115,6 +122,20 @@ func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (User, e
115122
return user, json.NewDecoder(res.Body).Decode(&user)
116123
}
117124

125+
// UpdateUserProfile enables callers to update profile information
126+
func (c *Client) UpdateUserProfile(ctx context.Context, userID uuid.UUID, req UpdateUserProfileRequest) (User, error) {
127+
res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/profile", uuidOrMe(userID)), req)
128+
if err != nil {
129+
return User{}, err
130+
}
131+
defer res.Body.Close()
132+
if res.StatusCode != http.StatusOK {
133+
return User{}, readBodyAsError(res)
134+
}
135+
var user User
136+
return user, json.NewDecoder(res.Body).Decode(&user)
137+
}
138+
118139
// CreateAPIKey generates an API key for the user ID provided.
119140
func (c *Client) CreateAPIKey(ctx context.Context, userID uuid.UUID) (*GenerateAPIKeyResponse, error) {
120141
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", uuidOrMe(userID)), nil)

0 commit comments

Comments
 (0)