mirror of https://github.com/coder/coder.git
feat: Add update profile endpoint (#916)
This commit is contained in:
parent
db9d5b7e8c
commit
63d1465019
|
@ -150,6 +150,7 @@ func New(options *Options) (http.Handler, func()) {
|
|||
r.Route("/{user}", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractUserParam(options.Database))
|
||||
r.Get("/", api.userByName)
|
||||
r.Put("/profile", api.putUserProfile)
|
||||
r.Get("/organizations", api.organizationsByUser)
|
||||
r.Post("/organizations", api.postOrganizationsByUser)
|
||||
r.Post("/keys", api.postAPIKey)
|
||||
|
|
|
@ -1050,6 +1050,23 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
|
|||
return user, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, user := range q.users {
|
||||
if user.ID != arg.ID {
|
||||
continue
|
||||
}
|
||||
user.Name = arg.Name
|
||||
user.Email = arg.Email
|
||||
user.Username = arg.Username
|
||||
q.users[index] = user
|
||||
return user, nil
|
||||
}
|
||||
return database.User{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWorkspaceParams) (database.Workspace, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
|
|
@ -81,6 +81,7 @@ type querier interface {
|
|||
UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error
|
||||
UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error
|
||||
UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error
|
||||
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error)
|
||||
UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error
|
||||
UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error
|
||||
UpdateWorkspaceAutostop(ctx context.Context, arg UpdateWorkspaceAutostopParams) error
|
||||
|
|
|
@ -1905,6 +1905,49 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
|
|||
return i, err
|
||||
}
|
||||
|
||||
const updateUserProfile = `-- name: UpdateUserProfile :one
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
email = $2,
|
||||
"name" = $3,
|
||||
username = $4,
|
||||
updated_at = $5
|
||||
WHERE
|
||||
id = $1 RETURNING id, email, name, revoked, login_type, hashed_password, created_at, updated_at, username
|
||||
`
|
||||
|
||||
type UpdateUserProfileParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Email string `db:"email" json:"email"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Username string `db:"username" json:"username"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateUserProfile,
|
||||
arg.ID,
|
||||
arg.Email,
|
||||
arg.Name,
|
||||
arg.Username,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.Name,
|
||||
&i.Revoked,
|
||||
&i.LoginType,
|
||||
&i.HashedPassword,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Username,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceAgentByAuthToken = `-- name: GetWorkspaceAgentByAuthToken :one
|
||||
SELECT
|
||||
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata
|
||||
|
|
|
@ -40,3 +40,14 @@ INSERT INTO
|
|||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, FALSE, $5, $6, $7, $8) RETURNING *;
|
||||
|
||||
-- name: UpdateUserProfile :one
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
email = $2,
|
||||
"name" = $3,
|
||||
username = $4,
|
||||
updated_at = $5
|
||||
WHERE
|
||||
id = $1 RETURNING *;
|
||||
|
|
|
@ -270,6 +270,70 @@ func (*api) userByName(rw http.ResponseWriter, r *http.Request) {
|
|||
render.JSON(rw, r, convertUser(user))
|
||||
}
|
||||
|
||||
func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
|
||||
var params codersdk.UpdateUserProfileRequest
|
||||
if !httpapi.Read(rw, r, ¶ms) {
|
||||
return
|
||||
}
|
||||
|
||||
if params.Name == nil {
|
||||
params.Name = &user.Name
|
||||
}
|
||||
|
||||
existentUser, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
||||
Email: params.Email,
|
||||
Username: params.Username,
|
||||
})
|
||||
isDifferentUser := existentUser.ID != user.ID
|
||||
|
||||
if err == nil && isDifferentUser {
|
||||
responseErrors := []httpapi.Error{}
|
||||
if existentUser.Email == params.Email {
|
||||
responseErrors = append(responseErrors, httpapi.Error{
|
||||
Field: "email",
|
||||
Code: "exists",
|
||||
})
|
||||
}
|
||||
if existentUser.Username == params.Username {
|
||||
responseErrors = append(responseErrors, httpapi.Error{
|
||||
Field: "username",
|
||||
Code: "exists",
|
||||
})
|
||||
}
|
||||
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
||||
Message: fmt.Sprintf("user already exists"),
|
||||
Errors: responseErrors,
|
||||
})
|
||||
return
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) && isDifferentUser {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get user: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
updatedUserProfile, err := api.Database.UpdateUserProfile(r.Context(), database.UpdateUserProfileParams{
|
||||
ID: user.ID,
|
||||
Name: *params.Name,
|
||||
Email: params.Email,
|
||||
Username: params.Username,
|
||||
UpdatedAt: database.Now(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("patch user: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(rw, r, convertUser(updatedUserProfile))
|
||||
}
|
||||
|
||||
// Returns organizations the parameterized user has access to.
|
||||
func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
|
@ -872,5 +936,6 @@ func convertUser(user database.User) codersdk.User {
|
|||
Email: user.Email,
|
||||
CreatedAt: user.CreatedAt,
|
||||
Username: user.Username,
|
||||
Name: user.Name,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -200,6 +200,111 @@ func TestPostUsers(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestUpdateUserProfile(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("UserNotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.UpdateUserProfile(context.Background(), uuid.New(), codersdk.UpdateUserProfileRequest{
|
||||
Username: "newusername",
|
||||
Email: "newemail@coder.com",
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
// Right now, we are raising a BAD request error because we don't support a
|
||||
// user accessing other users info
|
||||
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("ConflictingEmail", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
existentUser, _ := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
||||
Email: "bruno@coder.com",
|
||||
Username: "bruno",
|
||||
Password: "password",
|
||||
OrganizationID: user.OrganizationID,
|
||||
})
|
||||
_, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
|
||||
Username: "newusername",
|
||||
Email: existentUser.Email,
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("ConflictingUsername", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
existentUser, _ := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
||||
Email: "bruno@coder.com",
|
||||
Username: "bruno",
|
||||
Password: "password",
|
||||
OrganizationID: user.OrganizationID,
|
||||
})
|
||||
_, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
|
||||
Username: existentUser.Username,
|
||||
Email: "newemail@coder.com",
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("UpdateUsernameAndEmail", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
userProfile, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
|
||||
Username: "newusername",
|
||||
Email: "newemail@coder.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, userProfile.Username, "newusername")
|
||||
require.Equal(t, userProfile.Email, "newemail@coder.com")
|
||||
})
|
||||
|
||||
t.Run("UpdateUsername", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
me, _ := client.User(context.Background(), codersdk.Me)
|
||||
userProfile, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
|
||||
Username: me.Username,
|
||||
Email: "newemail@coder.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, userProfile.Username, me.Username)
|
||||
require.Equal(t, userProfile.Email, "newemail@coder.com")
|
||||
})
|
||||
|
||||
t.Run("KeepUserName", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
me, _ := client.User(context.Background(), codersdk.Me)
|
||||
newName := "New Name"
|
||||
firstProfile, _ := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
|
||||
Username: me.Username,
|
||||
Email: me.Email,
|
||||
Name: &newName,
|
||||
})
|
||||
t.Log(firstProfile)
|
||||
userProfile, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
|
||||
Username: "newusername",
|
||||
Email: "newemail@coder.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, userProfile.Username, "newusername")
|
||||
require.Equal(t, userProfile.Email, "newemail@coder.com")
|
||||
require.Equal(t, userProfile.Name, newName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserByName(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
|
|
|
@ -19,6 +19,7 @@ type User struct {
|
|||
Email string `json:"email" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type CreateFirstUserRequest struct {
|
||||
|
@ -41,6 +42,12 @@ type CreateUserRequest struct {
|
|||
OrganizationID uuid.UUID `json:"organization_id" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateUserProfileRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,username"`
|
||||
Name *string `json:"name"`
|
||||
}
|
||||
|
||||
// LoginWithPasswordRequest enables callers to authenticate with email and password.
|
||||
type LoginWithPasswordRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
|
@ -115,6 +122,20 @@ func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (User, e
|
|||
return user, json.NewDecoder(res.Body).Decode(&user)
|
||||
}
|
||||
|
||||
// UpdateUserProfile enables callers to update profile information
|
||||
func (c *Client) UpdateUserProfile(ctx context.Context, userID uuid.UUID, req UpdateUserProfileRequest) (User, error) {
|
||||
res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/profile", uuidOrMe(userID)), req)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return User{}, readBodyAsError(res)
|
||||
}
|
||||
var user User
|
||||
return user, json.NewDecoder(res.Body).Decode(&user)
|
||||
}
|
||||
|
||||
// CreateAPIKey generates an API key for the user ID provided.
|
||||
func (c *Client) CreateAPIKey(ctx context.Context, userID uuid.UUID) (*GenerateAPIKeyResponse, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", uuidOrMe(userID)), nil)
|
||||
|
|
Loading…
Reference in New Issue