Skip to content

Commit 2e68b76

Browse files
committed
feat: add "Full Name" field to user creation
Adds the ability to specify "Full Name" (a.k.a. Name) when creating users either via CLI or UI.
1 parent 1369002 commit 2e68b76

32 files changed

+446
-25
lines changed

Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -865,3 +865,7 @@ test-tailnet-integration:
865865
test-clean:
866866
go clean -testcache
867867
.PHONY: test-clean
868+
869+
.PHONY: test-e2e
870+
test-e2e:
871+
cd ./site && DEBUG=pw:api pnpm playwright:test --forbid-only --workers 1

cli/login.go

+28
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ func promptFirstUsername(inv *serpent.Invocation) (string, error) {
5858
return username, nil
5959
}
6060

61+
func promptFirstName(inv *serpent.Invocation) (string, error) {
62+
name, err := cliui.Prompt(inv, cliui.PromptOptions{
63+
Text: "(Optional) What " + pretty.Sprint(cliui.DefaultStyles.Field, "name") + " would you like?",
64+
Default: "",
65+
})
66+
if err != nil {
67+
if errors.Is(err, cliui.Canceled) {
68+
return "", nil
69+
}
70+
return "", err
71+
}
72+
73+
return name, nil
74+
}
75+
6176
func promptFirstPassword(inv *serpent.Invocation) (string, error) {
6277
retry:
6378
password, err := cliui.Prompt(inv, cliui.PromptOptions{
@@ -130,6 +145,7 @@ func (r *RootCmd) login() *serpent.Command {
130145
var (
131146
email string
132147
username string
148+
name string
133149
password string
134150
trial bool
135151
useTokenForSession bool
@@ -191,6 +207,7 @@ func (r *RootCmd) login() *serpent.Command {
191207

192208
_, _ = fmt.Fprintf(inv.Stdout, "Attempting to authenticate with %s URL: '%s'\n", urlSource, serverURL)
193209

210+
// nolint: nestif
194211
if !hasFirstUser {
195212
_, _ = fmt.Fprintf(inv.Stdout, Caret+"Your Coder deployment hasn't been set up!\n")
196213

@@ -212,6 +229,10 @@ func (r *RootCmd) login() *serpent.Command {
212229
if err != nil {
213230
return err
214231
}
232+
name, err = promptFirstName(inv)
233+
if err != nil {
234+
return err
235+
}
215236
}
216237

217238
if email == "" {
@@ -249,6 +270,7 @@ func (r *RootCmd) login() *serpent.Command {
249270
_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
250271
Email: email,
251272
Username: username,
273+
Name: name,
252274
Password: password,
253275
Trial: trial,
254276
})
@@ -360,6 +382,12 @@ func (r *RootCmd) login() *serpent.Command {
360382
Description: "Specifies a username to use if creating the first user for the deployment.",
361383
Value: serpent.StringOf(&username),
362384
},
385+
{
386+
Flag: "first-user-full-name",
387+
Env: "CODER_FIRST_USER_FULL_NAME",
388+
Description: "Specifies a human-readable name for the first user of the deployment.",
389+
Value: serpent.StringOf(&name),
390+
},
363391
{
364392
Flag: "first-user-password",
365393
Env: "CODER_FIRST_USER_PASSWORD",

cli/login_test.go

+133-16
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/coder/coder/v2/coderd/coderdtest"
2121
"github.com/coder/coder/v2/codersdk"
2222
"github.com/coder/coder/v2/pty/ptytest"
23+
"github.com/coder/coder/v2/testutil"
2324
)
2425

2526
func TestLogin(t *testing.T) {
@@ -91,10 +92,11 @@ func TestLogin(t *testing.T) {
9192

9293
matches := []string{
9394
"first user?", "yes",
94-
"username", "testuser",
95-
"email", "user@coder.com",
96-
"password", "SomeSecurePassword!",
97-
"password", "SomeSecurePassword!", // Confirm.
95+
"username", coderdtest.FirstUserParams.Username,
96+
"name", coderdtest.FirstUserParams.Name,
97+
"email", coderdtest.FirstUserParams.Email,
98+
"password", coderdtest.FirstUserParams.Password,
99+
"password", coderdtest.FirstUserParams.Password, // confirm
98100
"trial", "yes",
99101
}
100102
for i := 0; i < len(matches); i += 2 {
@@ -105,6 +107,64 @@ func TestLogin(t *testing.T) {
105107
}
106108
pty.ExpectMatch("Welcome to Coder")
107109
<-doneChan
110+
ctx := testutil.Context(t, testutil.WaitShort)
111+
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
112+
Email: coderdtest.FirstUserParams.Email,
113+
Password: coderdtest.FirstUserParams.Password,
114+
})
115+
require.NoError(t, err)
116+
client.SetSessionToken(resp.SessionToken)
117+
me, err := client.User(ctx, codersdk.Me)
118+
require.NoError(t, err)
119+
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
120+
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
121+
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
122+
})
123+
124+
t.Run("InitialUserTTYNameOptional", func(t *testing.T) {
125+
t.Parallel()
126+
client := coderdtest.New(t, nil)
127+
// The --force-tty flag is required on Windows, because the `isatty` library does not
128+
// accurately detect Windows ptys when they are not attached to a process:
129+
// https://github.com/mattn/go-isatty/issues/59
130+
doneChan := make(chan struct{})
131+
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String())
132+
pty := ptytest.New(t).Attach(root)
133+
go func() {
134+
defer close(doneChan)
135+
err := root.Run()
136+
assert.NoError(t, err)
137+
}()
138+
139+
matches := []string{
140+
"first user?", "yes",
141+
"username", coderdtest.FirstUserParams.Username,
142+
"name", "",
143+
"email", coderdtest.FirstUserParams.Email,
144+
"password", coderdtest.FirstUserParams.Password,
145+
"password", coderdtest.FirstUserParams.Password, // confirm
146+
"trial", "yes",
147+
}
148+
for i := 0; i < len(matches); i += 2 {
149+
match := matches[i]
150+
value := matches[i+1]
151+
pty.ExpectMatch(match)
152+
pty.WriteLine(value)
153+
}
154+
pty.ExpectMatch("Welcome to Coder")
155+
<-doneChan
156+
ctx := testutil.Context(t, testutil.WaitShort)
157+
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
158+
Email: coderdtest.FirstUserParams.Email,
159+
Password: coderdtest.FirstUserParams.Password,
160+
})
161+
require.NoError(t, err)
162+
client.SetSessionToken(resp.SessionToken)
163+
me, err := client.User(ctx, codersdk.Me)
164+
require.NoError(t, err)
165+
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
166+
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
167+
assert.Empty(t, me.Name)
108168
})
109169

110170
t.Run("InitialUserTTYFlag", func(t *testing.T) {
@@ -121,10 +181,11 @@ func TestLogin(t *testing.T) {
121181
pty.ExpectMatch(fmt.Sprintf("Attempting to authenticate with flag URL: '%s'", client.URL.String()))
122182
matches := []string{
123183
"first user?", "yes",
124-
"username", "testuser",
125-
"email", "user@coder.com",
126-
"password", "SomeSecurePassword!",
127-
"password", "SomeSecurePassword!", // Confirm.
184+
"username", coderdtest.FirstUserParams.Username,
185+
"name", coderdtest.FirstUserParams.Name,
186+
"email", coderdtest.FirstUserParams.Email,
187+
"password", coderdtest.FirstUserParams.Password,
188+
"password", coderdtest.FirstUserParams.Password, // confirm
128189
"trial", "yes",
129190
}
130191
for i := 0; i < len(matches); i += 2 {
@@ -134,20 +195,75 @@ func TestLogin(t *testing.T) {
134195
pty.WriteLine(value)
135196
}
136197
pty.ExpectMatch("Welcome to Coder")
198+
ctx := testutil.Context(t, testutil.WaitShort)
199+
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
200+
Email: coderdtest.FirstUserParams.Email,
201+
Password: coderdtest.FirstUserParams.Password,
202+
})
203+
require.NoError(t, err)
204+
client.SetSessionToken(resp.SessionToken)
205+
me, err := client.User(ctx, codersdk.Me)
206+
require.NoError(t, err)
207+
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
208+
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
209+
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
137210
})
138211

139212
t.Run("InitialUserFlags", func(t *testing.T) {
140213
t.Parallel()
141214
client := coderdtest.New(t, nil)
142215
inv, _ := clitest.New(
143216
t, "login", client.URL.String(),
144-
"--first-user-username", "testuser", "--first-user-email", "user@coder.com",
145-
"--first-user-password", "SomeSecurePassword!", "--first-user-trial",
217+
"--first-user-username", coderdtest.FirstUserParams.Username,
218+
"--first-user-full-name", coderdtest.FirstUserParams.Name,
219+
"--first-user-email", coderdtest.FirstUserParams.Email,
220+
"--first-user-password", coderdtest.FirstUserParams.Password,
221+
"--first-user-trial",
222+
)
223+
pty := ptytest.New(t).Attach(inv)
224+
w := clitest.StartWithWaiter(t, inv)
225+
pty.ExpectMatch("Welcome to Coder")
226+
w.RequireSuccess()
227+
ctx := testutil.Context(t, testutil.WaitShort)
228+
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
229+
Email: coderdtest.FirstUserParams.Email,
230+
Password: coderdtest.FirstUserParams.Password,
231+
})
232+
require.NoError(t, err)
233+
client.SetSessionToken(resp.SessionToken)
234+
me, err := client.User(ctx, codersdk.Me)
235+
require.NoError(t, err)
236+
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
237+
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
238+
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
239+
})
240+
241+
t.Run("InitialUserFlagsNameOptional", func(t *testing.T) {
242+
t.Parallel()
243+
client := coderdtest.New(t, nil)
244+
inv, _ := clitest.New(
245+
t, "login", client.URL.String(),
246+
"--first-user-username", coderdtest.FirstUserParams.Username,
247+
"--first-user-email", coderdtest.FirstUserParams.Email,
248+
"--first-user-password", coderdtest.FirstUserParams.Password,
249+
"--first-user-trial",
146250
)
147251
pty := ptytest.New(t).Attach(inv)
148252
w := clitest.StartWithWaiter(t, inv)
149253
pty.ExpectMatch("Welcome to Coder")
150254
w.RequireSuccess()
255+
ctx := testutil.Context(t, testutil.WaitShort)
256+
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
257+
Email: coderdtest.FirstUserParams.Email,
258+
Password: coderdtest.FirstUserParams.Password,
259+
})
260+
require.NoError(t, err)
261+
client.SetSessionToken(resp.SessionToken)
262+
me, err := client.User(ctx, codersdk.Me)
263+
require.NoError(t, err)
264+
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
265+
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
266+
assert.Empty(t, me.Name)
151267
})
152268

153269
t.Run("InitialUserTTYConfirmPasswordFailAndReprompt", func(t *testing.T) {
@@ -169,10 +285,11 @@ func TestLogin(t *testing.T) {
169285

170286
matches := []string{
171287
"first user?", "yes",
172-
"username", "testuser",
173-
"email", "user@coder.com",
174-
"password", "MyFirstSecurePassword!",
175-
"password", "MyNonMatchingSecurePassword!", // Confirm.
288+
"username", coderdtest.FirstUserParams.Username,
289+
"name", coderdtest.FirstUserParams.Name,
290+
"email", coderdtest.FirstUserParams.Email,
291+
"password", coderdtest.FirstUserParams.Password,
292+
"password", "something completely different",
176293
}
177294
for i := 0; i < len(matches); i += 2 {
178295
match := matches[i]
@@ -185,9 +302,9 @@ func TestLogin(t *testing.T) {
185302
pty.ExpectMatch("Passwords do not match")
186303
pty.ExpectMatch("Enter a " + pretty.Sprint(cliui.DefaultStyles.Field, "password"))
187304

188-
pty.WriteLine("SomeSecurePassword!")
305+
pty.WriteLine(coderdtest.FirstUserParams.Password)
189306
pty.ExpectMatch("Confirm")
190-
pty.WriteLine("SomeSecurePassword!")
307+
pty.WriteLine(coderdtest.FirstUserParams.Password)
191308
pty.ExpectMatch("trial")
192309
pty.WriteLine("yes")
193310
pty.ExpectMatch("Welcome to Coder")

cli/server_createadminuser.go

+3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
8585
// Use the validator tags so we match the API's validation.
8686
req := codersdk.CreateUserRequest{
8787
Username: "username",
88+
Name: "Admin User",
8889
Email: "email@coder.com",
8990
Password: "ValidPa$$word123!",
9091
OrganizationID: uuid.New(),
@@ -116,6 +117,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
116117
return err
117118
}
118119
}
120+
119121
if newUserEmail == "" {
120122
newUserEmail, err = cliui.Prompt(inv, cliui.PromptOptions{
121123
Text: "Email",
@@ -189,6 +191,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
189191
ID: uuid.New(),
190192
Email: newUserEmail,
191193
Username: newUserUsername,
194+
Name: "Admin User",
192195
HashedPassword: []byte(hashedPassword),
193196
CreatedAt: dbtime.Now(),
194197
UpdatedAt: dbtime.Now(),

cli/testdata/coder_login_--help.golden

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ OPTIONS:
1010
Specifies an email address to use if creating the first user for the
1111
deployment.
1212

13+
--first-user-full-name string, $CODER_FIRST_USER_FULL_NAME
14+
Specifies a human-readable name for the first user of the deployment.
15+
1316
--first-user-password string, $CODER_FIRST_USER_PASSWORD
1417
Specifies a password to use if creating the first user for the
1518
deployment.

cli/testdata/coder_users_create_--help.golden

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ OPTIONS:
77
-e, --email string
88
Specifies an email address for the new user.
99

10+
-n, --full-name string
11+
Specifies an optional human-readable name for the new user.
12+
1013
--login-type string
1114
Optionally specify the login type for the user. Valid values are:
1215
password, none, github, oidc. Using 'none' prevents the user from

cli/testdata/coder_users_list_--output_json.golden

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"id": "[first user ID]",
44
"username": "testuser",
55
"avatar_url": "",
6-
"name": "",
6+
"name": "Test User",
77
"email": "testuser@coder.com",
88
"created_at": "[timestamp]",
99
"last_seen_at": "[timestamp]",

0 commit comments

Comments
 (0)