Skip to content

Commit 1bed8c8

Browse files
committed
feat: Add suspend/active user to cli
1 parent 64a8b4a commit 1bed8c8

File tree

8 files changed

+244
-53
lines changed

8 files changed

+244
-53
lines changed

cli/userlist.go

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@ package cli
22

33
import (
44
"fmt"
5-
"time"
65

7-
"github.com/jedib0t/go-pretty/v6/table"
86
"github.com/spf13/cobra"
97

10-
"github.com/coder/coder/cli/cliui"
118
"github.com/coder/coder/codersdk"
129
)
1310

@@ -28,25 +25,11 @@ func userList() *cobra.Command {
2825
return err
2926
}
3027

31-
tableWriter := cliui.Table()
32-
header := table.Row{"Username", "Email", "Created At"}
33-
tableWriter.AppendHeader(header)
34-
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, columns))
35-
tableWriter.SortBy([]table.SortBy{{
36-
Name: "Username",
37-
}})
38-
for _, user := range users {
39-
tableWriter.AppendRow(table.Row{
40-
user.Username,
41-
user.Email,
42-
user.CreatedAt.Format(time.Stamp),
43-
})
44-
}
45-
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())
28+
_, err = fmt.Fprintln(cmd.OutOrStdout(), DisplayUsers(columns, users...))
4629
return err
4730
},
4831
}
49-
cmd.Flags().StringArrayVarP(&columns, "column", "c", nil,
32+
cmd.Flags().StringArrayVarP(&columns, "column", "c", []string{"Username", "Email", "Created At"},
5033
"Specify a column to filter in the table.")
5134
return cmd
5235
}

cli/users.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,43 @@
11
package cli
22

3-
import "github.com/spf13/cobra"
3+
import (
4+
"time"
5+
6+
"github.com/coder/coder/cli/cliui"
7+
"github.com/coder/coder/codersdk"
8+
"github.com/jedib0t/go-pretty/v6/table"
9+
"github.com/spf13/cobra"
10+
)
411

512
func users() *cobra.Command {
613
cmd := &cobra.Command{
714
Short: "Create, remove, and list users",
815
Use: "users",
916
}
10-
cmd.AddCommand(userCreate(), userList())
17+
cmd.AddCommand(
18+
userCreate(),
19+
userList(),
20+
userStatus(),
21+
)
1122
return cmd
1223
}
24+
25+
func DisplayUsers(filterColumns []string, users ...codersdk.User) string {
26+
tableWriter := cliui.Table()
27+
header := table.Row{"ID", "Username", "Email", "Created At", "Status"}
28+
tableWriter.AppendHeader(header)
29+
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, filterColumns))
30+
tableWriter.SortBy([]table.SortBy{{
31+
Name: "Username",
32+
}})
33+
for _, user := range users {
34+
tableWriter.AppendRow(table.Row{
35+
user.ID.String(),
36+
user.Username,
37+
user.Email,
38+
user.CreatedAt.Format(time.Stamp),
39+
user.Status,
40+
})
41+
}
42+
return tableWriter.Render()
43+
}

cli/userstatus.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/coder/coder/codersdk"
7+
8+
"github.com/coder/coder/cli/cliui"
9+
"github.com/spf13/cobra"
10+
"golang.org/x/xerrors"
11+
)
12+
13+
func userStatus() *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "status",
16+
Short: "Update the status of a user",
17+
}
18+
cmd.AddCommand(
19+
setUserStatus(codersdk.UserStatusActive),
20+
setUserStatus(codersdk.UserStatusSuspended),
21+
)
22+
return cmd
23+
}
24+
25+
// setUserStatus sets a user status.
26+
func setUserStatus(sdkStatus codersdk.UserStatus) *cobra.Command {
27+
var verb string
28+
switch sdkStatus {
29+
case codersdk.UserStatusActive:
30+
verb = "active"
31+
case codersdk.UserStatusSuspended:
32+
verb = "suspend"
33+
default:
34+
panic(fmt.Sprintf("%s is not supported", sdkStatus))
35+
}
36+
37+
var (
38+
columns []string
39+
)
40+
cmd := &cobra.Command{
41+
Use: fmt.Sprintf("%s <username|user_id>", verb),
42+
Short: fmt.Sprintf("Update a user's status to %q", sdkStatus),
43+
Args: cobra.ExactArgs(1),
44+
Example: fmt.Sprintf("coder status %s example_user", verb),
45+
RunE: func(cmd *cobra.Command, args []string) error {
46+
client, err := createClient(cmd)
47+
if err != nil {
48+
return err
49+
}
50+
51+
identifier := args[0]
52+
if identifier == "" {
53+
return xerrors.Errorf("user identifier cannot be an empty string")
54+
}
55+
56+
user, err := client.UserByIdentifier(cmd.Context(), identifier)
57+
if err != nil {
58+
return xerrors.Errorf("fetch user: %w", err)
59+
}
60+
61+
// Display the user
62+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), DisplayUsers(columns, user))
63+
64+
// User status is already set to this
65+
if user.Status == sdkStatus {
66+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "User status is already %q\n", sdkStatus)
67+
return nil
68+
}
69+
70+
// Prompt to confirm the action
71+
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
72+
Text: fmt.Sprintf("Are you sure you want to %s this user?", verb),
73+
IsConfirm: true,
74+
Default: "yes",
75+
})
76+
if err != nil {
77+
return err
78+
}
79+
80+
_, err = client.SetUserStatus(cmd.Context(), user.ID, sdkStatus)
81+
if err != nil {
82+
return xerrors.Errorf("%s user: %w", verb, err)
83+
}
84+
return nil
85+
},
86+
}
87+
cmd.Flags().StringArrayVarP(&columns, "column", "c", []string{"Username", "Email", "Created At", "Status"},
88+
"Specify a column to filter in the table.")
89+
return cmd
90+
}

cli/userstatus_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"testing"
7+
8+
"github.com/coder/coder/codersdk"
9+
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/coder/coder/cli/clitest"
13+
"github.com/coder/coder/coderd/coderdtest"
14+
)
15+
16+
func TestUserStatus(t *testing.T) {
17+
t.Parallel()
18+
client := coderdtest.New(t, nil)
19+
admin := coderdtest.CreateFirstUser(t, client)
20+
other := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
21+
otherUser, err := other.User(context.Background(), codersdk.Me)
22+
require.NoError(t, err, "fetch user")
23+
24+
t.Run("StatusSelf", func(t *testing.T) {
25+
cmd, root := clitest.New(t, "users", "status", "suspend", "me")
26+
clitest.SetupConfig(t, client, root)
27+
// Yes to the prompt
28+
cmd.SetIn(bytes.NewReader([]byte("yes\n")))
29+
err := cmd.Execute()
30+
// Expect an error, as you cannot suspend yourself
31+
require.Error(t, err)
32+
require.ErrorContains(t, err, "cannot suspend yourself")
33+
})
34+
35+
t.Run("StatusOther", func(t *testing.T) {
36+
require.Equal(t, otherUser.Status, codersdk.UserStatusActive, "start as active")
37+
38+
cmd, root := clitest.New(t, "users", "status", "suspend", otherUser.Username)
39+
clitest.SetupConfig(t, client, root)
40+
// Yes to the prompt
41+
cmd.SetIn(bytes.NewReader([]byte("yes\n")))
42+
err := cmd.Execute()
43+
require.NoError(t, err, "suspend user")
44+
45+
// Check the user status
46+
otherUser, err = client.User(context.Background(), otherUser.ID)
47+
require.NoError(t, err, "fetch suspended user")
48+
require.Equal(t, otherUser.Status, codersdk.UserStatusSuspended, "suspended user")
49+
50+
// Set back to active
51+
cmd, root = clitest.New(t, "users", "status", "active", otherUser.Username)
52+
clitest.SetupConfig(t, client, root)
53+
// Yes to the prompt
54+
cmd.SetIn(bytes.NewReader([]byte("yes\n")))
55+
err = cmd.Execute()
56+
require.NoError(t, err, "suspend user")
57+
58+
// Check the user status
59+
otherUser, err = client.User(context.Background(), otherUser.ID)
60+
require.NoError(t, err, "fetch active user")
61+
require.Equal(t, otherUser.Status, codersdk.UserStatusActive, "active user")
62+
})
63+
}

coderd/coderd.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,10 @@ func New(options *Options) (http.Handler, func()) {
239239
r.Use(httpmw.ExtractUserParam(options.Database))
240240
r.Get("/", api.userByName)
241241
r.Put("/profile", api.putUserProfile)
242-
r.Put("/suspend", api.putUserSuspend)
242+
r.Route("/status", func(r chi.Router) {
243+
r.Put("/suspend", api.putUserStatus(database.UserStatusSuspended))
244+
r.Put("/active", api.putUserStatus(database.UserStatusActive))
245+
})
243246
r.Route("/password", func(r chi.Router) {
244247
r.Use(httpmw.WithRBACObject(rbac.ResourceUserPasswordRole))
245248
r.Put("/", authorize(api.putUserPassword, rbac.ActionUpdate))

coderd/users.go

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -303,31 +303,40 @@ func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) {
303303
httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile, organizationIDs))
304304
}
305305

306-
func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) {
307-
user := httpmw.UserParam(r)
308-
309-
suspendedUser, err := api.Database.UpdateUserStatus(r.Context(), database.UpdateUserStatusParams{
310-
ID: user.ID,
311-
Status: database.UserStatusSuspended,
312-
UpdatedAt: database.Now(),
313-
})
306+
func (api *api) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) {
307+
return func(rw http.ResponseWriter, r *http.Request) {
308+
user := httpmw.UserParam(r)
309+
apiKey := httpmw.APIKey(r)
310+
if status == database.UserStatusSuspended && user.ID == apiKey.UserID {
311+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
312+
Message: fmt.Sprintf("You cannot suspend yourself"),
313+
})
314+
return
315+
}
314316

315-
if err != nil {
316-
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
317-
Message: fmt.Sprintf("put user suspended: %s", err.Error()),
317+
suspendedUser, err := api.Database.UpdateUserStatus(r.Context(), database.UpdateUserStatusParams{
318+
ID: user.ID,
319+
Status: status,
320+
UpdatedAt: database.Now(),
318321
})
319-
return
320-
}
321322

322-
organizations, err := userOrganizationIDs(r.Context(), api, user)
323-
if err != nil {
324-
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
325-
Message: fmt.Sprintf("get organization IDs: %s", err.Error()),
326-
})
327-
return
328-
}
323+
if err != nil {
324+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
325+
Message: fmt.Sprintf("put user suspended: %s", err.Error()),
326+
})
327+
return
328+
}
329329

330-
httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser, organizations))
330+
organizations, err := userOrganizationIDs(r.Context(), api, user)
331+
if err != nil {
332+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
333+
Message: fmt.Sprintf("get organization IDs: %s", err.Error()),
334+
})
335+
return
336+
}
337+
338+
httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser, organizations))
339+
}
331340
}
332341

333342
func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) {

coderd/users_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ func TestPutUserSuspend(t *testing.T) {
452452
Password: "password",
453453
OrganizationID: me.OrganizationID,
454454
})
455-
user, err := client.SuspendUser(context.Background(), user.ID)
455+
user, err := client.SetUserStatus(context.Background(), user.ID, codersdk.UserStatusSuspended)
456456
require.NoError(t, err)
457457
require.Equal(t, user.Status, codersdk.UserStatusSuspended)
458458
})
@@ -462,7 +462,7 @@ func TestPutUserSuspend(t *testing.T) {
462462
client := coderdtest.New(t, nil)
463463
coderdtest.CreateFirstUser(t, client)
464464
client.User(context.Background(), codersdk.Me)
465-
suspendedUser, err := client.SuspendUser(context.Background(), codersdk.Me)
465+
suspendedUser, err := client.SetUserStatus(context.Background(), codersdk.Me, codersdk.UserStatusSuspended)
466466

467467
require.NoError(t, err)
468468
require.Equal(t, suspendedUser.Status, codersdk.UserStatusSuspended)
@@ -504,7 +504,7 @@ func TestGetUser(t *testing.T) {
504504
exp, err := client.User(context.Background(), firstUser.UserID)
505505
require.NoError(t, err)
506506

507-
user, err := client.UserByUsername(context.Background(), exp.Username)
507+
user, err := client.UserByIdentifier(context.Background(), exp.Username)
508508
require.NoError(t, err)
509509
require.Equal(t, exp, user)
510510
})
@@ -551,7 +551,7 @@ func TestGetUsers(t *testing.T) {
551551
require.NoError(t, err)
552552
active = append(active, bruno)
553553

554-
_, err = client.SuspendUser(context.Background(), first.UserID)
554+
_, err = client.SetUserStatus(context.Background(), first.UserID, codersdk.UserStatusSuspended)
555555
require.NoError(t, err)
556556

557557
users, err := client.Users(context.Background(), codersdk.UsersRequest{

0 commit comments

Comments
 (0)