From 8c35a6e60194323293f2c548c23ac33cb80825e0 Mon Sep 17 00:00:00 2001 From: Bruno Date: Mon, 25 Apr 2022 13:42:41 +0000 Subject: [PATCH 01/10] chore: add sql query to suspend/active the user --- coderd/database/dump.sql | 3 +- .../migrations/000007_user_suspended.down.sql | 2 + .../migrations/000007_user_suspended.up.sql | 2 + coderd/database/models.go | 13 +++--- coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 46 +++++++++++++++++-- coderd/database/queries/users.sql | 9 ++++ 7 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 coderd/database/migrations/000007_user_suspended.down.sql create mode 100644 coderd/database/migrations/000007_user_suspended.up.sql diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index fbc137f1223e0..a2d01fa3fbe94 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -221,7 +221,8 @@ CREATE TABLE users ( username text DEFAULT ''::text NOT NULL, hashed_password bytea NOT NULL, created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL + updated_at timestamp with time zone NOT NULL, + suspended boolean DEFAULT false ); CREATE TABLE workspace_agents ( diff --git a/coderd/database/migrations/000007_user_suspended.down.sql b/coderd/database/migrations/000007_user_suspended.down.sql new file mode 100644 index 0000000000000..48ab9dd7ed7b7 --- /dev/null +++ b/coderd/database/migrations/000007_user_suspended.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE ONLY users + DROP COLUMN IF EXISTS suspended; diff --git a/coderd/database/migrations/000007_user_suspended.up.sql b/coderd/database/migrations/000007_user_suspended.up.sql new file mode 100644 index 0000000000000..508ea75d3967e --- /dev/null +++ b/coderd/database/migrations/000007_user_suspended.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE ONLY users + ADD COLUMN IF NOT EXISTS suspended boolean DEFAULT FALSE; diff --git a/coderd/database/models.go b/coderd/database/models.go index 56dc8fd77a58c..714a1563b2122 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -372,12 +372,13 @@ type TemplateVersion struct { } type User struct { - ID uuid.UUID `db:"id" json:"id"` - Email string `db:"email" json:"email"` - Username string `db:"username" json:"username"` - HashedPassword []byte `db:"hashed_password" json:"hashed_password"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + ID uuid.UUID `db:"id" json:"id"` + Email string `db:"email" json:"email"` + Username string `db:"username" json:"username"` + HashedPassword []byte `db:"hashed_password" json:"hashed_password"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Suspended sql.NullBool `db:"suspended" json:"suspended"` } type Workspace struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 3b8f317b620b3..16719fcf08724 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -84,6 +84,7 @@ type querier interface { UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) + UpdateUserSuspended(ctx context.Context, arg UpdateUserSuspendedParams) (User, error) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error UpdateWorkspaceAutostop(ctx context.Context, arg UpdateWorkspaceAutostopParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 3f76b362f4bb2..85b41cb53ed1e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1782,7 +1782,7 @@ func (q *sqlQuerier) UpdateTemplateVersionByID(ctx context.Context, arg UpdateTe const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one SELECT - id, email, username, hashed_password, created_at, updated_at + id, email, username, hashed_password, created_at, updated_at, suspended FROM users WHERE @@ -1807,13 +1807,14 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy &i.HashedPassword, &i.CreatedAt, &i.UpdatedAt, + &i.Suspended, ) return i, err } const getUserByID = `-- name: GetUserByID :one SELECT - id, email, username, hashed_password, created_at, updated_at + id, email, username, hashed_password, created_at, updated_at, suspended FROM users WHERE @@ -1832,6 +1833,7 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error &i.HashedPassword, &i.CreatedAt, &i.UpdatedAt, + &i.Suspended, ) return i, err } @@ -1852,7 +1854,7 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) { const getUsers = `-- name: GetUsers :many SELECT - id, email, username, hashed_password, created_at, updated_at + id, email, username, hashed_password, created_at, updated_at, suspended FROM users WHERE @@ -1922,6 +1924,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User, &i.HashedPassword, &i.CreatedAt, &i.UpdatedAt, + &i.Suspended, ); err != nil { return nil, err } @@ -1947,7 +1950,7 @@ INSERT INTO updated_at ) VALUES - ($1, $2, $3, $4, $5, $6) RETURNING id, email, username, hashed_password, created_at, updated_at + ($1, $2, $3, $4, $5, $6) RETURNING id, email, username, hashed_password, created_at, updated_at, suspended ` type InsertUserParams struct { @@ -1976,6 +1979,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User &i.HashedPassword, &i.CreatedAt, &i.UpdatedAt, + &i.Suspended, ) return i, err } @@ -1988,7 +1992,7 @@ SET username = $3, updated_at = $4 WHERE - id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at + id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, suspended ` type UpdateUserProfileParams struct { @@ -2013,6 +2017,38 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil &i.HashedPassword, &i.CreatedAt, &i.UpdatedAt, + &i.Suspended, + ) + return i, err +} + +const updateUserSuspended = `-- name: UpdateUserSuspended :one +UPDATE + users +SET + suspended = $2, + updated_at = $3 +WHERE + id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, suspended +` + +type UpdateUserSuspendedParams struct { + ID uuid.UUID `db:"id" json:"id"` + Suspended sql.NullBool `db:"suspended" json:"suspended"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +func (q *sqlQuerier) UpdateUserSuspended(ctx context.Context, arg UpdateUserSuspendedParams) (User, error) { + row := q.db.QueryRowContext(ctx, updateUserSuspended, arg.ID, arg.Suspended, arg.UpdatedAt) + var i User + err := row.Scan( + &i.ID, + &i.Email, + &i.Username, + &i.HashedPassword, + &i.CreatedAt, + &i.UpdatedAt, + &i.Suspended, ) return i, err } diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index c8cc39b42ff1f..f78aeb4ef6ab0 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -90,3 +90,12 @@ ORDER BY LIMIT -- A null limit means "no limit", so -1 means return all NULLIF(@limit_opt :: int, -1); + +-- name: UpdateUserSuspended :one +UPDATE + users +SET + suspended = $2, + updated_at = $3 +WHERE + id = $1 RETURNING *; From c4404cb10f24ad3bc29b7bf97dbda5990d59fe95 Mon Sep 17 00:00:00 2001 From: Bruno Date: Mon, 25 Apr 2022 13:48:51 +0000 Subject: [PATCH 02/10] chore: Update fake database to have the suspended action --- coderd/database/databasefake/databasefake.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index acf9f0913f3f1..de191766703ba 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1139,6 +1139,22 @@ func (q *fakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUs return database.User{}, sql.ErrNoRows } +func (q *fakeQuerier) UpdateUserSuspended(_ context.Context, arg database.UpdateUserSuspendedParams) (database.User, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, user := range q.users { + if user.ID != arg.ID { + continue + } + user.Suspended = arg.Suspended + user.UpdatedAt = arg.UpdatedAt + 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() From d4c75367ad02bcdc65107da4f752a2c116f1fef3 Mon Sep 17 00:00:00 2001 From: Bruno Date: Mon, 25 Apr 2022 14:41:16 +0000 Subject: [PATCH 03/10] feat: Add suspend user endpoint --- coderd/coderd.go | 1 + coderd/users.go | 19 +++++++++++++++++++ coderd/users_test.go | 26 ++++++++++++++++++++------ codersdk/users.go | 15 +++++++++++++++ 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 2f8bde36e4638..2e62da3c479d8 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -173,6 +173,7 @@ func New(options *Options) (http.Handler, func()) { r.Use(httpmw.ExtractUserParam(options.Database)) r.Get("/", api.userByName) r.Put("/profile", api.putUserProfile) + r.Put("/suspend", api.putUserSuspend) r.Get("/organizations", api.organizationsByUser) r.Post("/organizations", api.postOrganizationsByUser) r.Post("/keys", api.postAPIKey) diff --git a/coderd/users.go b/coderd/users.go index 579fcffd7e862..ddbc1bed9850d 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -283,6 +283,25 @@ func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile)) } +func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) { + user := httpmw.UserParam(r) + + suspendedUser, err := api.Database.UpdateUserSuspended(r.Context(), database.UpdateUserSuspendedParams{ + ID: user.ID, + Suspended: sql.NullBool{Bool: true}, + UpdatedAt: database.Now(), + }) + + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("put user suspended: %s", err.Error()), + }) + return + } + + httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser)) +} + // Returns organizations the parameterized user has access to. func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) diff --git a/coderd/users_test.go b/coderd/users_test.go index cb0139d1aed50..ac4e45c6ddf3f 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -274,17 +274,31 @@ func TestUpdateUserProfile(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", + user := coderdtest.CreateFirstUser(t, client) + anotherUser, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{ + Email: "bruno@coder.com", + Username: "bruno", + Password: "password", + OrganizationID: user.OrganizationID, }) + suspendedUser, err := client.SuspendUser(context.Background(), anotherUser.ID) require.NoError(t, err) - require.Equal(t, userProfile.Username, me.Username) - require.Equal(t, userProfile.Email, "newemail@coder.com") + require.Equal(t, suspendedUser.Suspended, true) }) } +func TestPutUserSuspend(t *testing.T) { + 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") +} + func TestUserByName(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) diff --git a/codersdk/users.go b/codersdk/users.go index da5f8f23fa475..5ff3ad7e9083c 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -34,6 +34,7 @@ type User struct { Email string `json:"email" validate:"required"` CreatedAt time.Time `json:"created_at" validate:"required"` Username string `json:"username" validate:"required"` + Suspended bool `json:"suspended"` } type CreateFirstUserRequest struct { @@ -155,6 +156,20 @@ func (c *Client) UpdateUserProfile(ctx context.Context, userID uuid.UUID, req Up return user, json.NewDecoder(res.Body).Decode(&user) } +// SuspendUser enables callers to suspend a user +func (c *Client) SuspendUser(ctx context.Context, userID uuid.UUID) (User, error) { + res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/suspend", uuidOrMe(userID)), nil) + 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) From 76a95d9fac2e6872723d625958f4c1ef554ba8ce Mon Sep 17 00:00:00 2001 From: Bruno Date: Mon, 25 Apr 2022 19:08:47 +0000 Subject: [PATCH 04/10] chore: replace suspened column by status --- coderd/database/databasefake/databasefake.go | 5 +- coderd/database/dump.sql | 7 ++- .../migrations/000007_user_status.down.sql | 4 ++ .../migrations/000007_user_status.up.sql | 4 ++ .../migrations/000007_user_suspended.down.sql | 2 - .../migrations/000007_user_suspended.up.sql | 2 - coderd/database/models.go | 33 ++++++++++--- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 40 ++++++++-------- coderd/database/queries/users.sql | 4 +- coderd/users.go | 5 +- coderd/users_test.go | 48 +++++++++++++------ codersdk/users.go | 17 +++++-- site/src/api/typesGenerated.ts | 19 ++++---- 14 files changed, 124 insertions(+), 68 deletions(-) create mode 100644 coderd/database/migrations/000007_user_status.down.sql create mode 100644 coderd/database/migrations/000007_user_status.up.sql delete mode 100644 coderd/database/migrations/000007_user_suspended.down.sql delete mode 100644 coderd/database/migrations/000007_user_suspended.up.sql diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index de191766703ba..1662110af758a 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1118,6 +1118,7 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam CreatedAt: arg.CreatedAt, UpdatedAt: arg.UpdatedAt, Username: arg.Username, + Status: database.UserStatusTypeActive, } q.users = append(q.users, user) return user, nil @@ -1139,7 +1140,7 @@ func (q *fakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUs return database.User{}, sql.ErrNoRows } -func (q *fakeQuerier) UpdateUserSuspended(_ context.Context, arg database.UpdateUserSuspendedParams) (database.User, error) { +func (q *fakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUserStatusParams) (database.User, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -1147,7 +1148,7 @@ func (q *fakeQuerier) UpdateUserSuspended(_ context.Context, arg database.Update if user.ID != arg.ID { continue } - user.Suspended = arg.Suspended + user.Status = arg.Status user.UpdatedAt = arg.UpdatedAt q.users[index] = user return user, nil diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index a2d01fa3fbe94..087b926cce10b 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -56,6 +56,11 @@ CREATE TYPE provisioner_type AS ENUM ( 'terraform' ); +CREATE TYPE user_status_type AS ENUM ( + 'active', + 'suspended' +); + CREATE TYPE workspace_transition AS ENUM ( 'start', 'stop', @@ -222,7 +227,7 @@ CREATE TABLE users ( hashed_password bytea NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, - suspended boolean DEFAULT false + status user_status_type DEFAULT 'active'::public.user_status_type NOT NULL ); CREATE TABLE workspace_agents ( diff --git a/coderd/database/migrations/000007_user_status.down.sql b/coderd/database/migrations/000007_user_status.down.sql new file mode 100644 index 0000000000000..5e1e05ec18e24 --- /dev/null +++ b/coderd/database/migrations/000007_user_status.down.sql @@ -0,0 +1,4 @@ +DROP TYPE user_status_type; + +ALTER TABLE ONLY users + DROP COLUMN IF EXISTS status; diff --git a/coderd/database/migrations/000007_user_status.up.sql b/coderd/database/migrations/000007_user_status.up.sql new file mode 100644 index 0000000000000..f877b0de55917 --- /dev/null +++ b/coderd/database/migrations/000007_user_status.up.sql @@ -0,0 +1,4 @@ +CREATE TYPE user_status_type AS ENUM ('active', 'suspended'); + +ALTER TABLE ONLY users + ADD COLUMN IF NOT EXISTS status user_status_type NOT NULL DEFAULT 'active'; diff --git a/coderd/database/migrations/000007_user_suspended.down.sql b/coderd/database/migrations/000007_user_suspended.down.sql deleted file mode 100644 index 48ab9dd7ed7b7..0000000000000 --- a/coderd/database/migrations/000007_user_suspended.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE ONLY users - DROP COLUMN IF EXISTS suspended; diff --git a/coderd/database/migrations/000007_user_suspended.up.sql b/coderd/database/migrations/000007_user_suspended.up.sql deleted file mode 100644 index 508ea75d3967e..0000000000000 --- a/coderd/database/migrations/000007_user_suspended.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE ONLY users - ADD COLUMN IF NOT EXISTS suspended boolean DEFAULT FALSE; diff --git a/coderd/database/models.go b/coderd/database/models.go index 714a1563b2122..76e9e293b5f96 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -208,6 +208,25 @@ func (e *ProvisionerType) Scan(src interface{}) error { return nil } +type UserStatusType string + +const ( + UserStatusTypeActive UserStatusType = "active" + UserStatusTypeSuspended UserStatusType = "suspended" +) + +func (e *UserStatusType) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = UserStatusType(s) + case string: + *e = UserStatusType(s) + default: + return fmt.Errorf("unsupported scan type for UserStatusType: %T", src) + } + return nil +} + type WorkspaceTransition string const ( @@ -372,13 +391,13 @@ type TemplateVersion struct { } type User struct { - ID uuid.UUID `db:"id" json:"id"` - Email string `db:"email" json:"email"` - Username string `db:"username" json:"username"` - HashedPassword []byte `db:"hashed_password" json:"hashed_password"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - Suspended sql.NullBool `db:"suspended" json:"suspended"` + ID uuid.UUID `db:"id" json:"id"` + Email string `db:"email" json:"email"` + Username string `db:"username" json:"username"` + HashedPassword []byte `db:"hashed_password" json:"hashed_password"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Status UserStatusType `db:"status" json:"status"` } type Workspace struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 16719fcf08724..970c6ad53d23b 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -84,7 +84,7 @@ type querier interface { UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) - UpdateUserSuspended(ctx context.Context, arg UpdateUserSuspendedParams) (User, error) + UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error UpdateWorkspaceAutostop(ctx context.Context, arg UpdateWorkspaceAutostopParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 85b41cb53ed1e..101a001bd4ebd 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1782,7 +1782,7 @@ func (q *sqlQuerier) UpdateTemplateVersionByID(ctx context.Context, arg UpdateTe const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one SELECT - id, email, username, hashed_password, created_at, updated_at, suspended + id, email, username, hashed_password, created_at, updated_at, status FROM users WHERE @@ -1807,14 +1807,14 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy &i.HashedPassword, &i.CreatedAt, &i.UpdatedAt, - &i.Suspended, + &i.Status, ) return i, err } const getUserByID = `-- name: GetUserByID :one SELECT - id, email, username, hashed_password, created_at, updated_at, suspended + id, email, username, hashed_password, created_at, updated_at, status FROM users WHERE @@ -1833,7 +1833,7 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error &i.HashedPassword, &i.CreatedAt, &i.UpdatedAt, - &i.Suspended, + &i.Status, ) return i, err } @@ -1854,7 +1854,7 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) { const getUsers = `-- name: GetUsers :many SELECT - id, email, username, hashed_password, created_at, updated_at, suspended + id, email, username, hashed_password, created_at, updated_at, status FROM users WHERE @@ -1924,7 +1924,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User, &i.HashedPassword, &i.CreatedAt, &i.UpdatedAt, - &i.Suspended, + &i.Status, ); err != nil { return nil, err } @@ -1950,7 +1950,7 @@ INSERT INTO updated_at ) VALUES - ($1, $2, $3, $4, $5, $6) RETURNING id, email, username, hashed_password, created_at, updated_at, suspended + ($1, $2, $3, $4, $5, $6) RETURNING id, email, username, hashed_password, created_at, updated_at, status ` type InsertUserParams struct { @@ -1979,7 +1979,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User &i.HashedPassword, &i.CreatedAt, &i.UpdatedAt, - &i.Suspended, + &i.Status, ) return i, err } @@ -1992,7 +1992,7 @@ SET username = $3, updated_at = $4 WHERE - id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, suspended + id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status ` type UpdateUserProfileParams struct { @@ -2017,29 +2017,29 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil &i.HashedPassword, &i.CreatedAt, &i.UpdatedAt, - &i.Suspended, + &i.Status, ) return i, err } -const updateUserSuspended = `-- name: UpdateUserSuspended :one +const updateUserStatus = `-- name: UpdateUserStatus :one UPDATE users SET - suspended = $2, + status = $2, updated_at = $3 WHERE - id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, suspended + id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status ` -type UpdateUserSuspendedParams struct { - ID uuid.UUID `db:"id" json:"id"` - Suspended sql.NullBool `db:"suspended" json:"suspended"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +type UpdateUserStatusParams struct { + ID uuid.UUID `db:"id" json:"id"` + Status UserStatusType `db:"status" json:"status"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } -func (q *sqlQuerier) UpdateUserSuspended(ctx context.Context, arg UpdateUserSuspendedParams) (User, error) { - row := q.db.QueryRowContext(ctx, updateUserSuspended, arg.ID, arg.Suspended, arg.UpdatedAt) +func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) { + row := q.db.QueryRowContext(ctx, updateUserStatus, arg.ID, arg.Status, arg.UpdatedAt) var i User err := row.Scan( &i.ID, @@ -2048,7 +2048,7 @@ func (q *sqlQuerier) UpdateUserSuspended(ctx context.Context, arg UpdateUserSusp &i.HashedPassword, &i.CreatedAt, &i.UpdatedAt, - &i.Suspended, + &i.Status, ) return i, err } diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index f78aeb4ef6ab0..bf99df6a3f785 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -91,11 +91,11 @@ LIMIT -- A null limit means "no limit", so -1 means return all NULLIF(@limit_opt :: int, -1); --- name: UpdateUserSuspended :one +-- name: UpdateUserStatus :one UPDATE users SET - suspended = $2, + status = $2, updated_at = $3 WHERE id = $1 RETURNING *; diff --git a/coderd/users.go b/coderd/users.go index ddbc1bed9850d..8687eab210b81 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -286,9 +286,9 @@ func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) { func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) - suspendedUser, err := api.Database.UpdateUserSuspended(r.Context(), database.UpdateUserSuspendedParams{ + suspendedUser, err := api.Database.UpdateUserStatus(r.Context(), database.UpdateUserStatusParams{ ID: user.ID, - Suspended: sql.NullBool{Bool: true}, + Status: database.UserStatusTypeSuspended, UpdatedAt: database.Now(), }) @@ -961,6 +961,7 @@ func convertUser(user database.User) codersdk.User { Email: user.Email, CreatedAt: user.CreatedAt, Username: user.Username, + Status: codersdk.UserStatus(user.Status), } } diff --git a/coderd/users_test.go b/coderd/users_test.go index ac4e45c6ddf3f..21e0e20de807b 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -274,29 +274,47 @@ func TestUpdateUserProfile(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) coderdtest.CreateFirstUser(t, client) - user := coderdtest.CreateFirstUser(t, client) - anotherUser, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{ + 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") + }) +} + +func TestPutUserSuspend(t *testing.T) { + t.Parallel() + + t.Run("SuspendAnotherUser", func(t *testing.T) { + t.Skip() + t.Parallel() + client := coderdtest.New(t, nil) + me := coderdtest.CreateFirstUser(t, client) + client.User(context.Background(), codersdk.Me) + user, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{ Email: "bruno@coder.com", Username: "bruno", Password: "password", - OrganizationID: user.OrganizationID, + OrganizationID: me.OrganizationID, }) - suspendedUser, err := client.SuspendUser(context.Background(), anotherUser.ID) + user, err = client.SuspendUser(context.Background(), user.ID) require.NoError(t, err) - require.Equal(t, suspendedUser.Suspended, true) + require.Equal(t, user.Status, codersdk.UserStatusSuspended) }) -} -func TestPutUserSuspend(t *testing.T) { - 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", + t.Run("SuspendItSelf", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + client.User(context.Background(), codersdk.Me) + suspendedUser, err := client.SuspendUser(context.Background(), codersdk.Me) + + require.NoError(t, err) + require.Equal(t, suspendedUser.Status, codersdk.UserStatusSuspended) }) - require.NoError(t, err) - require.Equal(t, userProfile.Username, "newusername") - require.Equal(t, userProfile.Email, "newemail@coder.com") } func TestUserByName(t *testing.T) { diff --git a/codersdk/users.go b/codersdk/users.go index 5ff3ad7e9083c..bbbbcb1bb934d 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -28,13 +28,20 @@ type UsersRequest struct { Offset int `json:"offset"` } +type UserStatus string + +const ( + UserStatusActive UserStatus = "active" + UserStatusSuspended UserStatus = "suspended" +) + // User represents a user in Coder. type User struct { - ID uuid.UUID `json:"id" validate:"required"` - Email string `json:"email" validate:"required"` - CreatedAt time.Time `json:"created_at" validate:"required"` - Username string `json:"username" validate:"required"` - Suspended bool `json:"suspended"` + ID uuid.UUID `json:"id" validate:"required"` + Email string `json:"email" validate:"required"` + CreatedAt time.Time `json:"created_at" validate:"required"` + Username string `json:"username" validate:"required"` + Status UserStatus `json:"status"` } type CreateFirstUserRequest struct { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 230e600eeeb18..c4e4d68d76086 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -82,9 +82,10 @@ export interface UsersRequest { export interface User { readonly email: string readonly username: string + readonly suspended: boolean } -// From codersdk/users.go:39:6. +// From codersdk/users.go:40:6. export interface CreateFirstUserRequest { readonly email: string readonly username: string @@ -92,46 +93,46 @@ export interface CreateFirstUserRequest { readonly organization: string } -// From codersdk/users.go:52:6. +// From codersdk/users.go:53:6. export interface CreateUserRequest { readonly email: string readonly username: string readonly password: string } -// From codersdk/users.go:59:6. +// From codersdk/users.go:60:6. export interface UpdateUserProfileRequest { readonly email: string readonly username: string } -// From codersdk/users.go:65:6. +// From codersdk/users.go:66:6. export interface LoginWithPasswordRequest { readonly email: string readonly password: string } -// From codersdk/users.go:71:6. +// From codersdk/users.go:72:6. export interface LoginWithPasswordResponse { readonly session_token: string } -// From codersdk/users.go:76:6. +// From codersdk/users.go:77:6. export interface GenerateAPIKeyResponse { readonly key: string } -// From codersdk/users.go:80:6. +// From codersdk/users.go:81:6. export interface CreateOrganizationRequest { readonly name: string } -// From codersdk/users.go:85:6. +// From codersdk/users.go:86:6. export interface CreateWorkspaceRequest { readonly name: string } -// From codersdk/users.go:94:6. +// From codersdk/users.go:95:6. export interface AuthMethods { readonly password: boolean readonly github: boolean From ee9f6b054efe6a49810d455f6657400c8113c40a Mon Sep 17 00:00:00 2001 From: Bruno Date: Mon, 25 Apr 2022 19:14:45 +0000 Subject: [PATCH 05/10] chore: fix ts types --- site/src/api/typesGenerated.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index c4e4d68d76086..a27592b834ef6 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -78,14 +78,14 @@ export interface UsersRequest { readonly offset: number } -// From codersdk/users.go:32:6. +// From codersdk/users.go:39:6. export interface User { readonly email: string readonly username: string - readonly suspended: boolean + readonly status: UserStatus } -// From codersdk/users.go:40:6. +// From codersdk/users.go:47:6. export interface CreateFirstUserRequest { readonly email: string readonly username: string @@ -93,46 +93,46 @@ export interface CreateFirstUserRequest { readonly organization: string } -// From codersdk/users.go:53:6. +// From codersdk/users.go:60:6. export interface CreateUserRequest { readonly email: string readonly username: string readonly password: string } -// From codersdk/users.go:60:6. +// From codersdk/users.go:67:6. export interface UpdateUserProfileRequest { readonly email: string readonly username: string } -// From codersdk/users.go:66:6. +// From codersdk/users.go:73:6. export interface LoginWithPasswordRequest { readonly email: string readonly password: string } -// From codersdk/users.go:72:6. +// From codersdk/users.go:79:6. export interface LoginWithPasswordResponse { readonly session_token: string } -// From codersdk/users.go:77:6. +// From codersdk/users.go:84:6. export interface GenerateAPIKeyResponse { readonly key: string } -// From codersdk/users.go:81:6. +// From codersdk/users.go:88:6. export interface CreateOrganizationRequest { readonly name: string } -// From codersdk/users.go:86:6. +// From codersdk/users.go:93:6. export interface CreateWorkspaceRequest { readonly name: string } -// From codersdk/users.go:95:6. +// From codersdk/users.go:102:6. export interface AuthMethods { readonly password: boolean readonly github: boolean @@ -235,5 +235,8 @@ export type ParameterScope = "organization" | "template" | "user" | "workspace" // From codersdk/provisionerdaemons.go:26:6. export type ProvisionerJobStatus = "pending" | "running" | "succeeded" | "canceling" | "canceled" | "failed" +// From codersdk/users.go:31:6. +export type UserStatus = "active" | "suspended" + // From codersdk/workspaceresources.go:15:6. export type WorkspaceAgentStatus = "connecting" | "connected" | "disconnected" From d0d69158a78c4b5fddceea81e23ce86a437e0cec Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 26 Apr 2022 13:24:02 +0000 Subject: [PATCH 06/10] refactor: rename user_status_type to user_status --- coderd/database/dump.sql | 4 +-- .../migrations/000007_user_status.down.sql | 2 +- .../migrations/000007_user_status.up.sql | 4 +-- coderd/database/models.go | 28 +++++++++---------- coderd/database/queries.sql.go | 6 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 087b926cce10b..16ce95964c157 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -56,7 +56,7 @@ CREATE TYPE provisioner_type AS ENUM ( 'terraform' ); -CREATE TYPE user_status_type AS ENUM ( +CREATE TYPE user_status AS ENUM ( 'active', 'suspended' ); @@ -227,7 +227,7 @@ CREATE TABLE users ( hashed_password bytea NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, - status user_status_type DEFAULT 'active'::public.user_status_type NOT NULL + status user_status DEFAULT 'active'::public.user_status NOT NULL ); CREATE TABLE workspace_agents ( diff --git a/coderd/database/migrations/000007_user_status.down.sql b/coderd/database/migrations/000007_user_status.down.sql index 5e1e05ec18e24..f3851331d6a67 100644 --- a/coderd/database/migrations/000007_user_status.down.sql +++ b/coderd/database/migrations/000007_user_status.down.sql @@ -1,4 +1,4 @@ -DROP TYPE user_status_type; +DROP TYPE user_status; ALTER TABLE ONLY users DROP COLUMN IF EXISTS status; diff --git a/coderd/database/migrations/000007_user_status.up.sql b/coderd/database/migrations/000007_user_status.up.sql index f877b0de55917..f0a791523d662 100644 --- a/coderd/database/migrations/000007_user_status.up.sql +++ b/coderd/database/migrations/000007_user_status.up.sql @@ -1,4 +1,4 @@ -CREATE TYPE user_status_type AS ENUM ('active', 'suspended'); +CREATE TYPE user_status AS ENUM ('active', 'suspended'); ALTER TABLE ONLY users - ADD COLUMN IF NOT EXISTS status user_status_type NOT NULL DEFAULT 'active'; + ADD COLUMN IF NOT EXISTS status user_status NOT NULL DEFAULT 'active'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 76e9e293b5f96..62256872f686a 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -208,21 +208,21 @@ func (e *ProvisionerType) Scan(src interface{}) error { return nil } -type UserStatusType string +type UserStatus string const ( - UserStatusTypeActive UserStatusType = "active" - UserStatusTypeSuspended UserStatusType = "suspended" + UserStatusActive UserStatus = "active" + UserStatusSuspended UserStatus = "suspended" ) -func (e *UserStatusType) Scan(src interface{}) error { +func (e *UserStatus) Scan(src interface{}) error { switch s := src.(type) { case []byte: - *e = UserStatusType(s) + *e = UserStatus(s) case string: - *e = UserStatusType(s) + *e = UserStatus(s) default: - return fmt.Errorf("unsupported scan type for UserStatusType: %T", src) + return fmt.Errorf("unsupported scan type for UserStatus: %T", src) } return nil } @@ -391,13 +391,13 @@ type TemplateVersion struct { } type User struct { - ID uuid.UUID `db:"id" json:"id"` - Email string `db:"email" json:"email"` - Username string `db:"username" json:"username"` - HashedPassword []byte `db:"hashed_password" json:"hashed_password"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - Status UserStatusType `db:"status" json:"status"` + ID uuid.UUID `db:"id" json:"id"` + Email string `db:"email" json:"email"` + Username string `db:"username" json:"username"` + HashedPassword []byte `db:"hashed_password" json:"hashed_password"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Status UserStatus `db:"status" json:"status"` } type Workspace struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 101a001bd4ebd..304ffb6cda9bc 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2033,9 +2033,9 @@ WHERE ` type UpdateUserStatusParams struct { - ID uuid.UUID `db:"id" json:"id"` - Status UserStatusType `db:"status" json:"status"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + ID uuid.UUID `db:"id" json:"id"` + Status UserStatus `db:"status" json:"status"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) { From e9a343452b024bd2b0bd9a03605ba31b401cc8cc Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 26 Apr 2022 13:25:35 +0000 Subject: [PATCH 07/10] fix: generated status types --- coderd/database/databasefake/databasefake.go | 2 +- coderd/users.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 1662110af758a..dc6c19d86455f 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1118,7 +1118,7 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam CreatedAt: arg.CreatedAt, UpdatedAt: arg.UpdatedAt, Username: arg.Username, - Status: database.UserStatusTypeActive, + Status: database.UserStatusActive, } q.users = append(q.users, user) return user, nil diff --git a/coderd/users.go b/coderd/users.go index 8687eab210b81..d7a8efce22077 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -288,7 +288,7 @@ func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) { suspendedUser, err := api.Database.UpdateUserStatus(r.Context(), database.UpdateUserStatusParams{ ID: user.ID, - Status: database.UserStatusTypeSuspended, + Status: database.UserStatusSuspended, UpdatedAt: database.Now(), }) From 45623c7308db0d13f9f826b068c6bd558e593e7b Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 26 Apr 2022 13:27:33 +0000 Subject: [PATCH 08/10] fix: fix lint --- cli/gitssh.go | 5 +++-- coderd/users_test.go | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cli/gitssh.go b/cli/gitssh.go index e88c24747c7fa..c3fca9b5839e8 100644 --- a/cli/gitssh.go +++ b/cli/gitssh.go @@ -7,10 +7,11 @@ import ( "os/exec" "strings" - "github.com/coder/coder/cli/cliui" - "github.com/coder/coder/codersdk" "github.com/spf13/cobra" "golang.org/x/xerrors" + + "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/codersdk" ) func gitssh() *cobra.Command { diff --git a/coderd/users_test.go b/coderd/users_test.go index 21e0e20de807b..4fe0d23d23edc 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -294,13 +294,13 @@ func TestPutUserSuspend(t *testing.T) { client := coderdtest.New(t, nil) me := coderdtest.CreateFirstUser(t, client) client.User(context.Background(), codersdk.Me) - user, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{ + user, _ := client.CreateUser(context.Background(), codersdk.CreateUserRequest{ Email: "bruno@coder.com", Username: "bruno", Password: "password", OrganizationID: me.OrganizationID, }) - user, err = client.SuspendUser(context.Background(), user.ID) + user, err := client.SuspendUser(context.Background(), user.ID) require.NoError(t, err) require.Equal(t, user.Status, codersdk.UserStatusSuspended) }) From a36c2225aa6f64e15002d37f76a32a00243b1882 Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 26 Apr 2022 13:28:35 +0000 Subject: [PATCH 09/10] fix: drop type after drop the column --- coderd/database/migrations/000007_user_status.down.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/migrations/000007_user_status.down.sql b/coderd/database/migrations/000007_user_status.down.sql index f3851331d6a67..ed764b23d89c4 100644 --- a/coderd/database/migrations/000007_user_status.down.sql +++ b/coderd/database/migrations/000007_user_status.down.sql @@ -1,4 +1,4 @@ -DROP TYPE user_status; - ALTER TABLE ONLY users DROP COLUMN IF EXISTS status; + +DROP TYPE user_status; From cdebd550a2865f649022a2edbd8d78301ff8299c Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 26 Apr 2022 13:50:13 +0000 Subject: [PATCH 10/10] fix: auditable table for status --- coderd/audit/table.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/audit/table.go b/coderd/audit/table.go index 1b4cf2a92f2e1..fe6e4dda7e043 100644 --- a/coderd/audit/table.go +++ b/coderd/audit/table.go @@ -41,6 +41,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{ "hashed_password": ActionSecret, // A user can change their own password. "created_at": ActionIgnore, // Never changes. "updated_at": ActionIgnore, // Changes, but is implicit and not helpful in a diff. + "status": ActionTrack, // A user can update another user status }, &database.Workspace{}: { "id": ActionIgnore, // Never changes.