mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add "Full Name" field to user creation (#13659)
Adds the ability to specify "Full Name" (a.k.a. Name) when creating users either via CLI or UI.
This commit is contained in:
@@ -865,3 +865,7 @@ test-tailnet-integration:
|
||||
test-clean:
|
||||
go clean -testcache
|
||||
.PHONY: test-clean
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e:
|
||||
cd ./site && DEBUG=pw:api pnpm playwright:test --forbid-only --workers 1
|
||||
|
||||
@@ -58,6 +58,21 @@ func promptFirstUsername(inv *serpent.Invocation) (string, error) {
|
||||
return username, nil
|
||||
}
|
||||
|
||||
func promptFirstName(inv *serpent.Invocation) (string, error) {
|
||||
name, err := cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: "(Optional) What " + pretty.Sprint(cliui.DefaultStyles.Field, "name") + " would you like?",
|
||||
Default: "",
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, cliui.Canceled) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func promptFirstPassword(inv *serpent.Invocation) (string, error) {
|
||||
retry:
|
||||
password, err := cliui.Prompt(inv, cliui.PromptOptions{
|
||||
@@ -130,6 +145,7 @@ func (r *RootCmd) login() *serpent.Command {
|
||||
var (
|
||||
email string
|
||||
username string
|
||||
name string
|
||||
password string
|
||||
trial bool
|
||||
useTokenForSession bool
|
||||
@@ -191,6 +207,7 @@ func (r *RootCmd) login() *serpent.Command {
|
||||
|
||||
_, _ = fmt.Fprintf(inv.Stdout, "Attempting to authenticate with %s URL: '%s'\n", urlSource, serverURL)
|
||||
|
||||
// nolint: nestif
|
||||
if !hasFirstUser {
|
||||
_, _ = fmt.Fprintf(inv.Stdout, Caret+"Your Coder deployment hasn't been set up!\n")
|
||||
|
||||
@@ -212,6 +229,10 @@ func (r *RootCmd) login() *serpent.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, err = promptFirstName(inv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if email == "" {
|
||||
@@ -249,6 +270,7 @@ func (r *RootCmd) login() *serpent.Command {
|
||||
_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
|
||||
Email: email,
|
||||
Username: username,
|
||||
Name: name,
|
||||
Password: password,
|
||||
Trial: trial,
|
||||
})
|
||||
@@ -360,6 +382,12 @@ func (r *RootCmd) login() *serpent.Command {
|
||||
Description: "Specifies a username to use if creating the first user for the deployment.",
|
||||
Value: serpent.StringOf(&username),
|
||||
},
|
||||
{
|
||||
Flag: "first-user-full-name",
|
||||
Env: "CODER_FIRST_USER_FULL_NAME",
|
||||
Description: "Specifies a human-readable name for the first user of the deployment.",
|
||||
Value: serpent.StringOf(&name),
|
||||
},
|
||||
{
|
||||
Flag: "first-user-password",
|
||||
Env: "CODER_FIRST_USER_PASSWORD",
|
||||
|
||||
+133
-16
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/pty/ptytest"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
@@ -91,10 +92,11 @@ func TestLogin(t *testing.T) {
|
||||
|
||||
matches := []string{
|
||||
"first user?", "yes",
|
||||
"username", "testuser",
|
||||
"email", "user@coder.com",
|
||||
"password", "SomeSecurePassword!",
|
||||
"password", "SomeSecurePassword!", // Confirm.
|
||||
"username", coderdtest.FirstUserParams.Username,
|
||||
"name", coderdtest.FirstUserParams.Name,
|
||||
"email", coderdtest.FirstUserParams.Email,
|
||||
"password", coderdtest.FirstUserParams.Password,
|
||||
"password", coderdtest.FirstUserParams.Password, // confirm
|
||||
"trial", "yes",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
@@ -105,6 +107,64 @@ func TestLogin(t *testing.T) {
|
||||
}
|
||||
pty.ExpectMatch("Welcome to Coder")
|
||||
<-doneChan
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
|
||||
Email: coderdtest.FirstUserParams.Email,
|
||||
Password: coderdtest.FirstUserParams.Password,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
client.SetSessionToken(resp.SessionToken)
|
||||
me, err := client.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
|
||||
})
|
||||
|
||||
t.Run("InitialUserTTYNameOptional", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
// The --force-tty flag is required on Windows, because the `isatty` library does not
|
||||
// accurately detect Windows ptys when they are not attached to a process:
|
||||
// https://github.com/mattn/go-isatty/issues/59
|
||||
doneChan := make(chan struct{})
|
||||
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String())
|
||||
pty := ptytest.New(t).Attach(root)
|
||||
go func() {
|
||||
defer close(doneChan)
|
||||
err := root.Run()
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
matches := []string{
|
||||
"first user?", "yes",
|
||||
"username", coderdtest.FirstUserParams.Username,
|
||||
"name", "",
|
||||
"email", coderdtest.FirstUserParams.Email,
|
||||
"password", coderdtest.FirstUserParams.Password,
|
||||
"password", coderdtest.FirstUserParams.Password, // confirm
|
||||
"trial", "yes",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
value := matches[i+1]
|
||||
pty.ExpectMatch(match)
|
||||
pty.WriteLine(value)
|
||||
}
|
||||
pty.ExpectMatch("Welcome to Coder")
|
||||
<-doneChan
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
|
||||
Email: coderdtest.FirstUserParams.Email,
|
||||
Password: coderdtest.FirstUserParams.Password,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
client.SetSessionToken(resp.SessionToken)
|
||||
me, err := client.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
|
||||
assert.Empty(t, me.Name)
|
||||
})
|
||||
|
||||
t.Run("InitialUserTTYFlag", func(t *testing.T) {
|
||||
@@ -121,10 +181,11 @@ func TestLogin(t *testing.T) {
|
||||
pty.ExpectMatch(fmt.Sprintf("Attempting to authenticate with flag URL: '%s'", client.URL.String()))
|
||||
matches := []string{
|
||||
"first user?", "yes",
|
||||
"username", "testuser",
|
||||
"email", "user@coder.com",
|
||||
"password", "SomeSecurePassword!",
|
||||
"password", "SomeSecurePassword!", // Confirm.
|
||||
"username", coderdtest.FirstUserParams.Username,
|
||||
"name", coderdtest.FirstUserParams.Name,
|
||||
"email", coderdtest.FirstUserParams.Email,
|
||||
"password", coderdtest.FirstUserParams.Password,
|
||||
"password", coderdtest.FirstUserParams.Password, // confirm
|
||||
"trial", "yes",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
@@ -134,6 +195,18 @@ func TestLogin(t *testing.T) {
|
||||
pty.WriteLine(value)
|
||||
}
|
||||
pty.ExpectMatch("Welcome to Coder")
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
|
||||
Email: coderdtest.FirstUserParams.Email,
|
||||
Password: coderdtest.FirstUserParams.Password,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
client.SetSessionToken(resp.SessionToken)
|
||||
me, err := client.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
|
||||
})
|
||||
|
||||
t.Run("InitialUserFlags", func(t *testing.T) {
|
||||
@@ -141,13 +214,56 @@ func TestLogin(t *testing.T) {
|
||||
client := coderdtest.New(t, nil)
|
||||
inv, _ := clitest.New(
|
||||
t, "login", client.URL.String(),
|
||||
"--first-user-username", "testuser", "--first-user-email", "user@coder.com",
|
||||
"--first-user-password", "SomeSecurePassword!", "--first-user-trial",
|
||||
"--first-user-username", coderdtest.FirstUserParams.Username,
|
||||
"--first-user-full-name", coderdtest.FirstUserParams.Name,
|
||||
"--first-user-email", coderdtest.FirstUserParams.Email,
|
||||
"--first-user-password", coderdtest.FirstUserParams.Password,
|
||||
"--first-user-trial",
|
||||
)
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
w := clitest.StartWithWaiter(t, inv)
|
||||
pty.ExpectMatch("Welcome to Coder")
|
||||
w.RequireSuccess()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
|
||||
Email: coderdtest.FirstUserParams.Email,
|
||||
Password: coderdtest.FirstUserParams.Password,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
client.SetSessionToken(resp.SessionToken)
|
||||
me, err := client.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
|
||||
})
|
||||
|
||||
t.Run("InitialUserFlagsNameOptional", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
inv, _ := clitest.New(
|
||||
t, "login", client.URL.String(),
|
||||
"--first-user-username", coderdtest.FirstUserParams.Username,
|
||||
"--first-user-email", coderdtest.FirstUserParams.Email,
|
||||
"--first-user-password", coderdtest.FirstUserParams.Password,
|
||||
"--first-user-trial",
|
||||
)
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
w := clitest.StartWithWaiter(t, inv)
|
||||
pty.ExpectMatch("Welcome to Coder")
|
||||
w.RequireSuccess()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
|
||||
Email: coderdtest.FirstUserParams.Email,
|
||||
Password: coderdtest.FirstUserParams.Password,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
client.SetSessionToken(resp.SessionToken)
|
||||
me, err := client.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
|
||||
assert.Empty(t, me.Name)
|
||||
})
|
||||
|
||||
t.Run("InitialUserTTYConfirmPasswordFailAndReprompt", func(t *testing.T) {
|
||||
@@ -169,10 +285,11 @@ func TestLogin(t *testing.T) {
|
||||
|
||||
matches := []string{
|
||||
"first user?", "yes",
|
||||
"username", "testuser",
|
||||
"email", "user@coder.com",
|
||||
"password", "MyFirstSecurePassword!",
|
||||
"password", "MyNonMatchingSecurePassword!", // Confirm.
|
||||
"username", coderdtest.FirstUserParams.Username,
|
||||
"name", coderdtest.FirstUserParams.Name,
|
||||
"email", coderdtest.FirstUserParams.Email,
|
||||
"password", coderdtest.FirstUserParams.Password,
|
||||
"password", "something completely different",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
@@ -185,9 +302,9 @@ func TestLogin(t *testing.T) {
|
||||
pty.ExpectMatch("Passwords do not match")
|
||||
pty.ExpectMatch("Enter a " + pretty.Sprint(cliui.DefaultStyles.Field, "password"))
|
||||
|
||||
pty.WriteLine("SomeSecurePassword!")
|
||||
pty.WriteLine(coderdtest.FirstUserParams.Password)
|
||||
pty.ExpectMatch("Confirm")
|
||||
pty.WriteLine("SomeSecurePassword!")
|
||||
pty.WriteLine(coderdtest.FirstUserParams.Password)
|
||||
pty.ExpectMatch("trial")
|
||||
pty.WriteLine("yes")
|
||||
pty.ExpectMatch("Welcome to Coder")
|
||||
|
||||
@@ -85,6 +85,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
|
||||
// Use the validator tags so we match the API's validation.
|
||||
req := codersdk.CreateUserRequest{
|
||||
Username: "username",
|
||||
Name: "Admin User",
|
||||
Email: "email@coder.com",
|
||||
Password: "ValidPa$$word123!",
|
||||
OrganizationID: uuid.New(),
|
||||
@@ -116,6 +117,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if newUserEmail == "" {
|
||||
newUserEmail, err = cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: "Email",
|
||||
@@ -189,6 +191,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
|
||||
ID: uuid.New(),
|
||||
Email: newUserEmail,
|
||||
Username: newUserUsername,
|
||||
Name: "Admin User",
|
||||
HashedPassword: []byte(hashedPassword),
|
||||
CreatedAt: dbtime.Now(),
|
||||
UpdatedAt: dbtime.Now(),
|
||||
|
||||
+3
@@ -10,6 +10,9 @@ OPTIONS:
|
||||
Specifies an email address to use if creating the first user for the
|
||||
deployment.
|
||||
|
||||
--first-user-full-name string, $CODER_FIRST_USER_FULL_NAME
|
||||
Specifies a human-readable name for the first user of the deployment.
|
||||
|
||||
--first-user-password string, $CODER_FIRST_USER_PASSWORD
|
||||
Specifies a password to use if creating the first user for the
|
||||
deployment.
|
||||
|
||||
@@ -7,6 +7,9 @@ OPTIONS:
|
||||
-e, --email string
|
||||
Specifies an email address for the new user.
|
||||
|
||||
-n, --full-name string
|
||||
Specifies an optional human-readable name for the new user.
|
||||
|
||||
--login-type string
|
||||
Optionally specify the login type for the user. Valid values are:
|
||||
password, none, github, oidc. Using 'none' prevents the user from
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"id": "[first user ID]",
|
||||
"username": "testuser",
|
||||
"avatar_url": "",
|
||||
"name": "",
|
||||
"name": "Test User",
|
||||
"email": "testuser@coder.com",
|
||||
"created_at": "[timestamp]",
|
||||
"last_seen_at": "[timestamp]",
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/coder/pretty"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
"github.com/coder/serpent"
|
||||
@@ -19,6 +20,7 @@ func (r *RootCmd) userCreate() *serpent.Command {
|
||||
var (
|
||||
email string
|
||||
username string
|
||||
name string
|
||||
password string
|
||||
disableLogin bool
|
||||
loginType string
|
||||
@@ -35,6 +37,9 @@ func (r *RootCmd) userCreate() *serpent.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// We only prompt for the full name if both username and email have not
|
||||
// been set. This is to avoid breaking existing non-interactive usage.
|
||||
shouldPromptName := username == "" && email == ""
|
||||
if username == "" {
|
||||
username, err = cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: "Username:",
|
||||
@@ -58,6 +63,18 @@ func (r *RootCmd) userCreate() *serpent.Command {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if name == "" && shouldPromptName {
|
||||
rawName, err := cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: "Full name (optional):",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name = httpapi.NormalizeRealUsername(rawName)
|
||||
if !strings.EqualFold(rawName, name) {
|
||||
cliui.Warnf(inv.Stderr, "Normalized name to %q", name)
|
||||
}
|
||||
}
|
||||
userLoginType := codersdk.LoginTypePassword
|
||||
if disableLogin && loginType != "" {
|
||||
return xerrors.New("You cannot specify both --disable-login and --login-type")
|
||||
@@ -79,6 +96,7 @@ func (r *RootCmd) userCreate() *serpent.Command {
|
||||
_, err = client.CreateUser(inv.Context(), codersdk.CreateUserRequest{
|
||||
Email: email,
|
||||
Username: username,
|
||||
Name: name,
|
||||
Password: password,
|
||||
OrganizationID: organization.ID,
|
||||
UserLoginType: userLoginType,
|
||||
@@ -127,6 +145,12 @@ Create a workspace `+pretty.Sprint(cliui.DefaultStyles.Code, "coder create")+`!
|
||||
Description: "Specifies a username for the new user.",
|
||||
Value: serpent.StringOf(&username),
|
||||
},
|
||||
{
|
||||
Flag: "full-name",
|
||||
FlagShorthand: "n",
|
||||
Description: "Specifies an optional human-readable name for the new user.",
|
||||
Value: serpent.StringOf(&name),
|
||||
},
|
||||
{
|
||||
Flag: "password",
|
||||
FlagShorthand: "p",
|
||||
|
||||
+88
-1
@@ -4,16 +4,19 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/pty/ptytest"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestUserCreate(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Prompts", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
inv, root := clitest.New(t, "users", "create")
|
||||
@@ -28,6 +31,7 @@ func TestUserCreate(t *testing.T) {
|
||||
matches := []string{
|
||||
"Username", "dean",
|
||||
"Email", "dean@coder.com",
|
||||
"Full name (optional):", "Mr. Dean Deanington",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
@@ -35,6 +39,89 @@ func TestUserCreate(t *testing.T) {
|
||||
pty.ExpectMatch(match)
|
||||
pty.WriteLine(value)
|
||||
}
|
||||
<-doneChan
|
||||
_ = testutil.RequireRecvCtx(ctx, t, doneChan)
|
||||
created, err := client.User(ctx, matches[1])
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, matches[1], created.Username)
|
||||
assert.Equal(t, matches[3], created.Email)
|
||||
assert.Equal(t, matches[5], created.Name)
|
||||
})
|
||||
|
||||
t.Run("PromptsNoName", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
inv, root := clitest.New(t, "users", "create")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
doneChan := make(chan struct{})
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
go func() {
|
||||
defer close(doneChan)
|
||||
err := inv.Run()
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
matches := []string{
|
||||
"Username", "noname",
|
||||
"Email", "noname@coder.com",
|
||||
"Full name (optional):", "",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
value := matches[i+1]
|
||||
pty.ExpectMatch(match)
|
||||
pty.WriteLine(value)
|
||||
}
|
||||
_ = testutil.RequireRecvCtx(ctx, t, doneChan)
|
||||
created, err := client.User(ctx, matches[1])
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, matches[1], created.Username)
|
||||
assert.Equal(t, matches[3], created.Email)
|
||||
assert.Empty(t, created.Name)
|
||||
})
|
||||
|
||||
t.Run("Args", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
args := []string{
|
||||
"users", "create",
|
||||
"-e", "dean@coder.com",
|
||||
"-u", "dean",
|
||||
"-n", "Mr. Dean Deanington",
|
||||
"-p", "1n5ecureP4ssw0rd!",
|
||||
}
|
||||
inv, root := clitest.New(t, args...)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
created, err := client.User(ctx, "dean")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, args[3], created.Email)
|
||||
assert.Equal(t, args[5], created.Username)
|
||||
assert.Equal(t, args[7], created.Name)
|
||||
})
|
||||
|
||||
t.Run("ArgsNoName", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
args := []string{
|
||||
"users", "create",
|
||||
"-e", "dean@coder.com",
|
||||
"-u", "dean",
|
||||
"-p", "1n5ecureP4ssw0rd!",
|
||||
}
|
||||
inv, root := clitest.New(t, args...)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
created, err := client.User(ctx, args[5])
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, args[3], created.Email)
|
||||
assert.Equal(t, args[5], created.Username)
|
||||
assert.Empty(t, created.Name)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ func (*userShowFormat) Format(_ context.Context, out interface{}) (string, error
|
||||
// Add rows for each of the user's fields.
|
||||
addRow("ID", user.ID.String())
|
||||
addRow("Username", user.Username)
|
||||
addRow("Full name", user.Name)
|
||||
addRow("Email", user.Email)
|
||||
addRow("Status", user.Status)
|
||||
addRow("Created At", user.CreatedAt.Format(time.Stamp))
|
||||
|
||||
@@ -57,7 +57,14 @@ func TestUserList(t *testing.T) {
|
||||
err := json.Unmarshal(buf.Bytes(), &users)
|
||||
require.NoError(t, err, "unmarshal JSON output")
|
||||
require.Len(t, users, 2)
|
||||
require.Contains(t, users[0].Email, "coder.com")
|
||||
for _, u := range users {
|
||||
assert.NotEmpty(t, u.ID)
|
||||
assert.NotEmpty(t, u.Email)
|
||||
assert.NotEmpty(t, u.Username)
|
||||
assert.NotEmpty(t, u.Name)
|
||||
assert.NotEmpty(t, u.CreatedAt)
|
||||
assert.NotEmpty(t, u.Status)
|
||||
}
|
||||
})
|
||||
t.Run("NoURLFileErrorHasHelperText", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -133,5 +140,6 @@ func TestUserShow(t *testing.T) {
|
||||
require.Equal(t, otherUser.ID, newUser.ID)
|
||||
require.Equal(t, otherUser.Username, newUser.Username)
|
||||
require.Equal(t, otherUser.Email, newUser.Email)
|
||||
require.Equal(t, otherUser.Name, newUser.Name)
|
||||
})
|
||||
}
|
||||
|
||||
Generated
+6
@@ -8425,6 +8425,9 @@ const docTemplate = `{
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8787,6 +8790,9 @@ const docTemplate = `{
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
||||
Generated
+6
@@ -7493,6 +7493,9 @@
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7824,6 +7827,9 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
"github.com/fullsailor/pkcs7"
|
||||
@@ -658,6 +659,7 @@ var FirstUserParams = codersdk.CreateFirstUserRequest{
|
||||
Email: "testuser@coder.com",
|
||||
Username: "testuser",
|
||||
Password: "SomeSecurePassword!",
|
||||
Name: "Test User",
|
||||
}
|
||||
|
||||
// CreateFirstUser creates a user with preset credentials and authenticates
|
||||
@@ -712,6 +714,7 @@ func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationI
|
||||
req := codersdk.CreateUserRequest{
|
||||
Email: namesgenerator.GetRandomName(10) + "@coder.com",
|
||||
Username: RandomUsername(t),
|
||||
Name: RandomName(t),
|
||||
Password: "SomeSecurePassword!",
|
||||
OrganizationID: organizationID,
|
||||
}
|
||||
@@ -1390,6 +1393,28 @@ func RandomUsername(t testing.TB) string {
|
||||
return n
|
||||
}
|
||||
|
||||
func RandomName(t testing.TB) string {
|
||||
var sb strings.Builder
|
||||
var err error
|
||||
ss := strings.Split(namesgenerator.GetRandomName(10), "_")
|
||||
for si, s := range ss {
|
||||
for ri, r := range s {
|
||||
if ri == 0 {
|
||||
_, err = sb.WriteRune(unicode.ToTitle(r))
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
_, err = sb.WriteRune(r)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
if si < len(ss)-1 {
|
||||
_, err = sb.WriteRune(' ')
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Used to easily create an HTTP transport!
|
||||
type roundTripper func(req *http.Request) (*http.Response, error)
|
||||
|
||||
|
||||
@@ -1122,6 +1122,7 @@ func (s *MethodTestSuite) TestUser() {
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
Username: u.Username,
|
||||
Name: u.Name,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
}).Asserts(u, policy.ActionUpdatePersonal).Returns(u)
|
||||
}))
|
||||
|
||||
@@ -289,6 +289,7 @@ func User(t testing.TB, db database.Store, orig database.User) database.User {
|
||||
ID: takeFirst(orig.ID, uuid.New()),
|
||||
Email: takeFirst(orig.Email, namesgenerator.GetRandomName(1)),
|
||||
Username: takeFirst(orig.Username, namesgenerator.GetRandomName(1)),
|
||||
Name: takeFirst(orig.Name, namesgenerator.GetRandomName(1)),
|
||||
HashedPassword: takeFirstSlice(orig.HashedPassword, []byte(must(cryptorand.String(32)))),
|
||||
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
|
||||
UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()),
|
||||
|
||||
@@ -322,6 +322,7 @@ func convertUsers(users []database.User, count int64) []database.GetUsersRow {
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
Username: u.Username,
|
||||
Name: u.Name,
|
||||
HashedPassword: u.HashedPassword,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
@@ -6492,6 +6493,7 @@ func (q *FakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
|
||||
CreatedAt: arg.CreatedAt,
|
||||
UpdatedAt: arg.UpdatedAt,
|
||||
Username: arg.Username,
|
||||
Name: arg.Name,
|
||||
Status: database.UserStatusDormant,
|
||||
RBACRoles: arg.RBACRoles,
|
||||
LoginType: arg.LoginType,
|
||||
|
||||
@@ -330,6 +330,7 @@ func ConvertUserRows(rows []GetUsersRow) []User {
|
||||
ID: r.ID,
|
||||
Email: r.Email,
|
||||
Username: r.Username,
|
||||
Name: r.Name,
|
||||
HashedPassword: r.HashedPassword,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
|
||||
@@ -8917,6 +8917,7 @@ INSERT INTO
|
||||
id,
|
||||
email,
|
||||
username,
|
||||
name,
|
||||
hashed_password,
|
||||
created_at,
|
||||
updated_at,
|
||||
@@ -8924,13 +8925,14 @@ INSERT INTO
|
||||
login_type
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
|
||||
`
|
||||
|
||||
type InsertUserParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Email string `db:"email" json:"email"`
|
||||
Username string `db:"username" json:"username"`
|
||||
Name string `db:"name" json:"name"`
|
||||
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"`
|
||||
@@ -8943,6 +8945,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
|
||||
arg.ID,
|
||||
arg.Email,
|
||||
arg.Username,
|
||||
arg.Name,
|
||||
arg.HashedPassword,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
|
||||
@@ -62,6 +62,7 @@ INSERT INTO
|
||||
id,
|
||||
email,
|
||||
username,
|
||||
name,
|
||||
hashed_password,
|
||||
created_at,
|
||||
updated_at,
|
||||
@@ -69,7 +70,7 @@ INSERT INTO
|
||||
login_type
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *;
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *;
|
||||
|
||||
-- name: UpdateUserProfile :one
|
||||
UPDATE
|
||||
|
||||
@@ -187,6 +187,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
||||
CreateUserRequest: codersdk.CreateUserRequest{
|
||||
Email: createUser.Email,
|
||||
Username: createUser.Username,
|
||||
Name: createUser.Name,
|
||||
Password: createUser.Password,
|
||||
OrganizationID: defaultOrg.ID,
|
||||
},
|
||||
@@ -1224,6 +1225,7 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
|
||||
ID: uuid.New(),
|
||||
Email: req.Email,
|
||||
Username: req.Username,
|
||||
Name: httpapi.NormalizeRealUsername(req.Name),
|
||||
CreatedAt: dbtime.Now(),
|
||||
UpdatedAt: dbtime.Now(),
|
||||
HashedPassword: []byte{},
|
||||
|
||||
@@ -70,8 +70,14 @@ func TestFirstUser(t *testing.T) {
|
||||
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
u, err := client.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Name, u.Name)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Email, u.Email)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Username, u.Username)
|
||||
})
|
||||
|
||||
t.Run("Trial", func(t *testing.T) {
|
||||
@@ -96,6 +102,7 @@ func TestFirstUser(t *testing.T) {
|
||||
req := codersdk.CreateFirstUserRequest{
|
||||
Email: "testuser@coder.com",
|
||||
Username: "testuser",
|
||||
Name: "Test User",
|
||||
Password: "SomeSecurePassword!",
|
||||
Trial: true,
|
||||
}
|
||||
@@ -1486,7 +1493,7 @@ func TestUsersFilter(t *testing.T) {
|
||||
exp = append(exp, made)
|
||||
}
|
||||
}
|
||||
require.ElementsMatch(t, exp, matched.Users, "expected workspaces returned")
|
||||
require.ElementsMatch(t, exp, matched.Users, "expected users returned")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ type LicensorTrialRequest struct {
|
||||
type CreateFirstUserRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,username"`
|
||||
Name string `json:"name" validate:"user_real_name"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Trial bool `json:"trial"`
|
||||
TrialInfo CreateFirstUserTrialInfo `json:"trial_info"`
|
||||
@@ -114,6 +115,7 @@ type CreateFirstUserResponse struct {
|
||||
type CreateUserRequest struct {
|
||||
Email string `json:"email" validate:"required,email" format:"email"`
|
||||
Username string `json:"username" validate:"required,username"`
|
||||
Name string `json:"name" validate:"user_real_name"`
|
||||
Password string `json:"password"`
|
||||
// UserLoginType defaults to LoginTypePassword.
|
||||
UserLoginType LoginType `json:"login_type"`
|
||||
|
||||
Generated
+4
@@ -938,6 +938,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
```json
|
||||
{
|
||||
"email": "string",
|
||||
"name": "string",
|
||||
"password": "string",
|
||||
"trial": true,
|
||||
"trial_info": {
|
||||
@@ -958,6 +959,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------ | ---------------------------------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `email` | string | true | | |
|
||||
| `name` | string | false | | |
|
||||
| `password` | string | true | | |
|
||||
| `trial` | boolean | false | | |
|
||||
| `trial_info` | [codersdk.CreateFirstUserTrialInfo](#codersdkcreatefirstusertrialinfo) | false | | |
|
||||
@@ -1248,6 +1250,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
"disable_login": true,
|
||||
"email": "user@example.com",
|
||||
"login_type": "",
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"password": "string",
|
||||
"username": "string"
|
||||
@@ -1261,6 +1264,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
| `disable_login` | boolean | false | | Disable login sets the user's login type to 'none'. This prevents the user from being able to use a password or any other authentication method to login. Deprecated: Set UserLoginType=LoginTypeDisabled instead. |
|
||||
| `email` | string | true | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | Login type defaults to LoginTypePassword. |
|
||||
| `name` | string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
| `password` | string | false | | |
|
||||
| `username` | string | true | | |
|
||||
|
||||
Generated
+2
@@ -83,6 +83,7 @@ curl -X POST http://coder-server:8080/api/v2/users \
|
||||
"disable_login": true,
|
||||
"email": "user@example.com",
|
||||
"login_type": "",
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"password": "string",
|
||||
"username": "string"
|
||||
@@ -229,6 +230,7 @@ curl -X POST http://coder-server:8080/api/v2/users/first \
|
||||
```json
|
||||
{
|
||||
"email": "string",
|
||||
"name": "string",
|
||||
"password": "string",
|
||||
"trial": true,
|
||||
"trial_info": {
|
||||
|
||||
Generated
+9
@@ -30,6 +30,15 @@ Specifies an email address to use if creating the first user for the deployment.
|
||||
|
||||
Specifies a username to use if creating the first user for the deployment.
|
||||
|
||||
### --first-user-full-name
|
||||
|
||||
| | |
|
||||
| ----------- | ---------------------------------------- |
|
||||
| Type | <code>string</code> |
|
||||
| Environment | <code>$CODER_FIRST_USER_FULL_NAME</code> |
|
||||
|
||||
Specifies a human-readable name for the first user of the deployment.
|
||||
|
||||
### --first-user-password
|
||||
|
||||
| | |
|
||||
|
||||
Generated
+8
@@ -26,6 +26,14 @@ Specifies an email address for the new user.
|
||||
|
||||
Specifies a username for the new user.
|
||||
|
||||
### -n, --full-name
|
||||
|
||||
| | |
|
||||
| ---- | ------------------- |
|
||||
| Type | <code>string</code> |
|
||||
|
||||
Specifies an optional human-readable name for the new user.
|
||||
|
||||
### -p, --password
|
||||
|
||||
| | |
|
||||
|
||||
+2
-2
@@ -155,7 +155,7 @@ fatal() {
|
||||
|
||||
if [ ! -f "${PROJECT_ROOT}/.coderv2/developsh-did-first-setup" ]; then
|
||||
# Try to create the initial admin user.
|
||||
if "${CODER_DEV_SHIM}" login http://127.0.0.1:3000 --first-user-username=admin --first-user-email=admin@coder.com --first-user-password="${password}" --first-user-trial=true; then
|
||||
if "${CODER_DEV_SHIM}" login http://127.0.0.1:3000 --first-user-username=admin --first-user-email=admin@coder.com --first-user-password="${password}" --first-user-full-name="Admin User" --first-user-trial=true; then
|
||||
# Only create this file if an admin user was successfully
|
||||
# created, otherwise we won't retry on a later attempt.
|
||||
touch "${PROJECT_ROOT}/.coderv2/developsh-did-first-setup"
|
||||
@@ -164,7 +164,7 @@ fatal() {
|
||||
fi
|
||||
|
||||
# Try to create a regular user.
|
||||
"${CODER_DEV_SHIM}" users create --email=member@coder.com --username=member --password="${password}" ||
|
||||
"${CODER_DEV_SHIM}" users create --email=member@coder.com --username=member --full-name "Regular User" --password="${password}" ||
|
||||
echo 'Failed to create regular user. To troubleshoot, try running this command manually.'
|
||||
fi
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export const createUser = async (orgId: string) => {
|
||||
const user = await API.createUser({
|
||||
email: `${name}@coder.com`,
|
||||
username: name,
|
||||
name: name,
|
||||
password: "s3cure&password!",
|
||||
login_type: "password",
|
||||
disable_login: false,
|
||||
|
||||
@@ -11,6 +11,38 @@ test("create user with password", async ({ page, baseURL }) => {
|
||||
await page.getByRole("button", { name: "Create user" }).click();
|
||||
await expect(page).toHaveTitle("Create User - Coder");
|
||||
|
||||
const name = randomName();
|
||||
const userValues = {
|
||||
username: name,
|
||||
name: name,
|
||||
email: `${name}@coder.com`,
|
||||
loginType: "password",
|
||||
password: "s3cure&password!",
|
||||
};
|
||||
|
||||
await page.getByLabel("Username").fill(userValues.username);
|
||||
await page.getByLabel("Full name").fill(userValues.username);
|
||||
await page.getByLabel("Email").fill(userValues.email);
|
||||
await page.getByLabel("Login Type").click();
|
||||
await page.getByRole("option", { name: "Password", exact: false }).click();
|
||||
// Using input[name=password] due to the select element utilizing 'password'
|
||||
// as the label for the currently active option.
|
||||
const passwordField = page.locator("input[name=password]");
|
||||
await passwordField.fill(userValues.password);
|
||||
await page.getByRole("button", { name: "Create user" }).click();
|
||||
await expect(page.getByText("Successfully created user.")).toBeVisible();
|
||||
|
||||
await expect(page).toHaveTitle("Users - Coder");
|
||||
await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible();
|
||||
});
|
||||
|
||||
test("create user without full name is optional", async ({ page, baseURL }) => {
|
||||
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
|
||||
await expect(page).toHaveTitle("Users - Coder");
|
||||
|
||||
await page.getByRole("button", { name: "Create user" }).click();
|
||||
await expect(page).toHaveTitle("Create User - Coder");
|
||||
|
||||
const name = randomName();
|
||||
const userValues = {
|
||||
username: name,
|
||||
|
||||
Generated
+2
@@ -194,6 +194,7 @@ export interface ConvertLoginRequest {
|
||||
export interface CreateFirstUserRequest {
|
||||
readonly email: string;
|
||||
readonly username: string;
|
||||
readonly name: string;
|
||||
readonly password: string;
|
||||
readonly trial: boolean;
|
||||
readonly trial_info: CreateFirstUserTrialInfo;
|
||||
@@ -294,6 +295,7 @@ export interface CreateTokenRequest {
|
||||
export interface CreateUserRequest {
|
||||
readonly email: string;
|
||||
readonly username: string;
|
||||
readonly name: string;
|
||||
readonly password: string;
|
||||
readonly login_type: LoginType;
|
||||
readonly disable_login: boolean;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { FormFooter } from "components/FormFooter/FormFooter";
|
||||
import { FullPageForm } from "components/FullPageForm/FullPageForm";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
displayNameValidator,
|
||||
getFormHelpers,
|
||||
nameValidator,
|
||||
onChangeTrimmed,
|
||||
@@ -20,6 +21,7 @@ export const Language = {
|
||||
emailLabel: "Email",
|
||||
passwordLabel: "Password",
|
||||
usernameLabel: "Username",
|
||||
nameLabel: "Full name",
|
||||
emailInvalid: "Please enter a valid email address.",
|
||||
emailRequired: "Please enter an email address.",
|
||||
passwordRequired: "Please enter a password.",
|
||||
@@ -78,6 +80,7 @@ const validationSchema = Yup.object({
|
||||
otherwise: (schema) => schema,
|
||||
}),
|
||||
username: nameValidator(Language.usernameLabel),
|
||||
name: displayNameValidator(Language.nameLabel),
|
||||
login_type: Yup.string().oneOf(Object.keys(authMethodLanguage)),
|
||||
});
|
||||
|
||||
@@ -90,6 +93,7 @@ export const CreateUserForm: FC<
|
||||
email: "",
|
||||
password: "",
|
||||
username: "",
|
||||
name: "",
|
||||
organization_id: organizationId,
|
||||
disable_login: false,
|
||||
login_type: "",
|
||||
@@ -124,6 +128,12 @@ export const CreateUserForm: FC<
|
||||
fullWidth
|
||||
label={Language.usernameLabel}
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
autoComplete="name"
|
||||
fullWidth
|
||||
label={Language.nameLabel}
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("email")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { countries } from "./countries";
|
||||
export const Language = {
|
||||
emailLabel: "Email",
|
||||
passwordLabel: "Password",
|
||||
nameLabel: "Full Name",
|
||||
usernameLabel: "Username",
|
||||
emailInvalid: "Please enter a valid email address.",
|
||||
emailRequired: "Please enter an email address.",
|
||||
@@ -96,6 +97,7 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
email: "",
|
||||
password: "",
|
||||
username: "",
|
||||
name: "",
|
||||
trial: false,
|
||||
trial_info: {
|
||||
first_name: "",
|
||||
@@ -152,6 +154,12 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
fullWidth
|
||||
label={Language.usernameLabel}
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
autoComplete="name"
|
||||
fullWidth
|
||||
label={Language.nameLabel}
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("email")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
@@ -167,7 +175,6 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
label={Language.passwordLabel}
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<label
|
||||
htmlFor="trial"
|
||||
css={{
|
||||
|
||||
Reference in New Issue
Block a user