Skip to content

Commit be974cf

Browse files
authored
feat: Add users create and list commands (coder#1111)
This allows for *extremely basic* user management.
1 parent 7496c3d commit be974cf

21 files changed

+245
-127
lines changed

cli/usercreate.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/go-playground/validator/v10"
7+
"github.com/spf13/cobra"
8+
"golang.org/x/xerrors"
9+
10+
"github.com/coder/coder/cli/cliui"
11+
"github.com/coder/coder/codersdk"
12+
"github.com/coder/coder/cryptorand"
13+
)
14+
15+
func userCreate() *cobra.Command {
16+
var (
17+
email string
18+
username string
19+
password string
20+
)
21+
cmd := &cobra.Command{
22+
Use: "create",
23+
RunE: func(cmd *cobra.Command, args []string) error {
24+
client, err := createClient(cmd)
25+
if err != nil {
26+
return err
27+
}
28+
organization, err := currentOrganization(cmd, client)
29+
if err != nil {
30+
return err
31+
}
32+
if username == "" {
33+
username, err = cliui.Prompt(cmd, cliui.PromptOptions{
34+
Text: "Username:",
35+
})
36+
if err != nil {
37+
return err
38+
}
39+
}
40+
if email == "" {
41+
email, err = cliui.Prompt(cmd, cliui.PromptOptions{
42+
Text: "Email:",
43+
Validate: func(s string) error {
44+
err := validator.New().Var(s, "email")
45+
if err != nil {
46+
return xerrors.New("That's not a valid email address!")
47+
}
48+
return err
49+
},
50+
})
51+
if err != nil {
52+
return err
53+
}
54+
}
55+
if password == "" {
56+
password, err = cryptorand.StringCharset(cryptorand.Human, 12)
57+
if err != nil {
58+
return err
59+
}
60+
}
61+
62+
_, err = client.CreateUser(cmd.Context(), codersdk.CreateUserRequest{
63+
Email: email,
64+
Username: username,
65+
Password: password,
66+
OrganizationID: organization.ID,
67+
})
68+
if err != nil {
69+
return err
70+
}
71+
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), `A new user has been created!
72+
Share the instructions below to get them started.
73+
`+cliui.Styles.Placeholder.Render("—————————————————————————————————————————————————")+`
74+
Download the Coder command line for your operating system:
75+
https://github.com/coder/coder/releases
76+
77+
Run `+cliui.Styles.Code.Render("coder login "+client.URL.String())+` to authenticate.
78+
79+
Your email is: `+cliui.Styles.Field.Render(email)+`
80+
Your password is: `+cliui.Styles.Field.Render(password)+`
81+
82+
Create a workspace `+cliui.Styles.Code.Render("coder workspaces create")+`!`)
83+
return nil
84+
},
85+
}
86+
cmd.Flags().StringVarP(&email, "email", "e", "", "Specifies an email address for the new user.")
87+
cmd.Flags().StringVarP(&username, "username", "u", "", "Specifies a username for the new user.")
88+
cmd.Flags().StringVarP(&password, "password", "p", "", "Specifies a password for the new user.")
89+
return cmd
90+
}

cli/usercreate_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package cli_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/coder/coder/cli/clitest"
9+
"github.com/coder/coder/coderd/coderdtest"
10+
"github.com/coder/coder/pty/ptytest"
11+
)
12+
13+
func TestUserCreate(t *testing.T) {
14+
t.Parallel()
15+
t.Run("Prompts", func(t *testing.T) {
16+
t.Parallel()
17+
client := coderdtest.New(t, nil)
18+
coderdtest.CreateFirstUser(t, client)
19+
cmd, root := clitest.New(t, "users", "create")
20+
clitest.SetupConfig(t, client, root)
21+
doneChan := make(chan struct{})
22+
pty := ptytest.New(t)
23+
cmd.SetIn(pty.Input())
24+
cmd.SetOut(pty.Output())
25+
go func() {
26+
defer close(doneChan)
27+
err := cmd.Execute()
28+
require.NoError(t, err)
29+
}()
30+
matches := []string{
31+
"Username", "dean",
32+
"Email", "dean@coder.com",
33+
}
34+
for i := 0; i < len(matches); i += 2 {
35+
match := matches[i]
36+
value := matches[i+1]
37+
pty.ExpectMatch(match)
38+
pty.WriteLine(value)
39+
}
40+
<-doneChan
41+
})
42+
}

cli/userlist.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
"time"
7+
8+
"github.com/jedib0t/go-pretty/v6/table"
9+
"github.com/spf13/cobra"
10+
11+
"github.com/coder/coder/codersdk"
12+
)
13+
14+
func userList() *cobra.Command {
15+
return &cobra.Command{
16+
Use: "list",
17+
Aliases: []string{"ls"},
18+
RunE: func(cmd *cobra.Command, args []string) error {
19+
client, err := createClient(cmd)
20+
if err != nil {
21+
return err
22+
}
23+
users, err := client.Users(cmd.Context(), codersdk.UsersRequest{})
24+
if err != nil {
25+
return err
26+
}
27+
sort.Slice(users, func(i, j int) bool {
28+
return users[i].Username < users[j].Username
29+
})
30+
31+
tableWriter := table.NewWriter()
32+
tableWriter.SetStyle(table.StyleLight)
33+
tableWriter.Style().Options.SeparateColumns = false
34+
tableWriter.AppendHeader(table.Row{"Username", "Email", "Created At"})
35+
for _, user := range users {
36+
tableWriter.AppendRow(table.Row{
37+
user.Username,
38+
user.Email,
39+
user.CreatedAt.Format(time.Stamp),
40+
})
41+
}
42+
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())
43+
return err
44+
},
45+
}
46+
}

cli/userlist_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package cli_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/coder/coder/cli/clitest"
9+
"github.com/coder/coder/coderd/coderdtest"
10+
"github.com/coder/coder/pty/ptytest"
11+
)
12+
13+
func TestUserList(t *testing.T) {
14+
t.Parallel()
15+
client := coderdtest.New(t, nil)
16+
coderdtest.CreateFirstUser(t, client)
17+
cmd, root := clitest.New(t, "users", "list")
18+
clitest.SetupConfig(t, client, root)
19+
doneChan := make(chan struct{})
20+
pty := ptytest.New(t)
21+
cmd.SetIn(pty.Input())
22+
cmd.SetOut(pty.Output())
23+
go func() {
24+
defer close(doneChan)
25+
err := cmd.Execute()
26+
require.NoError(t, err)
27+
}()
28+
pty.ExpectMatch("coder.com")
29+
<-doneChan
30+
}

cli/users.go

+1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ func users() *cobra.Command {
66
cmd := &cobra.Command{
77
Use: "users",
88
}
9+
cmd.AddCommand(userCreate(), userList())
910
return cmd
1011
}

coderd/database/databasefake/databasefake.go

-5
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,6 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
207207
tmp = append(tmp, users[i])
208208
} else if strings.Contains(user.Username, params.Search) {
209209
tmp = append(tmp, users[i])
210-
} else if strings.Contains(user.Name, params.Search) {
211-
tmp = append(tmp, users[i])
212210
}
213211
}
214212
users = tmp
@@ -1116,8 +1114,6 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
11161114
user := database.User{
11171115
ID: arg.ID,
11181116
Email: arg.Email,
1119-
Name: arg.Name,
1120-
LoginType: arg.LoginType,
11211117
HashedPassword: arg.HashedPassword,
11221118
CreatedAt: arg.CreatedAt,
11231119
UpdatedAt: arg.UpdatedAt,
@@ -1135,7 +1131,6 @@ func (q *fakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUs
11351131
if user.ID != arg.ID {
11361132
continue
11371133
}
1138-
user.Name = arg.Name
11391134
user.Email = arg.Email
11401135
user.Username = arg.Username
11411136
q.users[index] = user

coderd/database/dump.sql

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

coderd/database/migrations/000001_base.up.sql

+1-4
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,10 @@ CREATE TYPE login_type AS ENUM (
1212
CREATE TABLE IF NOT EXISTS users (
1313
id uuid NOT NULL,
1414
email text NOT NULL,
15-
name text NOT NULL,
16-
revoked boolean NOT NULL,
17-
login_type login_type NOT NULL,
15+
username text DEFAULT ''::text NOT NULL,
1816
hashed_password bytea NOT NULL,
1917
created_at timestamp with time zone NOT NULL,
2018
updated_at timestamp with time zone NOT NULL,
21-
username text DEFAULT ''::text NOT NULL,
2219
PRIMARY KEY (id)
2320
);
2421

coderd/database/models.go

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

0 commit comments

Comments
 (0)