From 22335c7cfa2c7bc3801fe9a26ef0842c8ae32527 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 6 Jun 2024 14:45:58 +0100 Subject: [PATCH 01/11] feat: add name field to setup screen --- cli/login.go | 27 ++++++++++++++++++++++ cli/login_test.go | 9 ++++++-- coderd/apidoc/docs.go | 3 +++ coderd/apidoc/swagger.json | 3 +++ coderd/coderdtest/coderdtest.go | 1 + coderd/database/dbmem/dbmem.go | 1 + coderd/users.go | 15 ++++++++++++ coderd/users_test.go | 9 +++++++- coderd/workspacebuilds_test.go | 2 +- codersdk/users.go | 1 + docs/api/schemas.md | 2 ++ docs/api/users.md | 1 + docs/cli/login.md | 9 ++++++++ site/src/api/typesGenerated.ts | 1 + site/src/pages/SetupPage/SetupPageView.tsx | 16 ++++++++++++- 15 files changed, 95 insertions(+), 5 deletions(-) diff --git a/cli/login.go b/cli/login.go index 65a94d8a4ec3e..82a8434ea5602 100644 --- a/cli/login.go +++ b/cli/login.go @@ -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 errors.Is(err, cliui.Canceled) { + return "", nil + } + if err != 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 @@ -212,6 +228,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 +269,7 @@ func (r *RootCmd) login() *serpent.Command { _, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{ Email: email, Username: username, + Name: name, Password: password, Trial: trial, }) @@ -353,6 +374,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-name", + Env: "CODER_FIRST_USER_AME", + Description: "Specifies a human-readable name for the first user of the deployment.", + Value: serpent.StringOf(&username), + }, { Flag: "first-user-password", Env: "CODER_FIRST_USER_PASSWORD", diff --git a/cli/login_test.go b/cli/login_test.go index 3cf9dc1945b57..ec846b14cfd51 100644 --- a/cli/login_test.go +++ b/cli/login_test.go @@ -90,6 +90,7 @@ func TestLogin(t *testing.T) { matches := []string{ "first user?", "yes", "username", "testuser", + "name", "Test User", "email", "user@coder.com", "password", "SomeSecurePassword!", "password", "SomeSecurePassword!", // Confirm. @@ -120,6 +121,7 @@ func TestLogin(t *testing.T) { matches := []string{ "first user?", "yes", "username", "testuser", + "name", "Test User", "email", "user@coder.com", "password", "SomeSecurePassword!", "password", "SomeSecurePassword!", // Confirm. @@ -139,8 +141,11 @@ 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", "testuser", + "--first-user-name", `'Test User'`, + "--first-user-email", "user@coder.com", + "--first-user-password", "SomeSecurePassword!", + "--first-user-trial", ) pty := ptytest.New(t).Attach(inv) w := clitest.StartWithWaiter(t, inv) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 26ddef4177d82..1088a27fbac7e 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8897,6 +8897,9 @@ const docTemplate = `{ "email": { "type": "string" }, + "name": { + "type": "string" + }, "password": { "type": "string" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 57800f5a38fa9..ccf35fc32d8ab 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7921,6 +7921,9 @@ "email": { "type": "string" }, + "name": { + "type": "string" + }, "password": { "type": "string" }, diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 316683a9f1e65..19a8092be1b71 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -645,6 +645,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 diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 2bfff39a949a9..311191b1a9630 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -320,6 +320,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, diff --git a/coderd/users.go b/coderd/users.go index 8db74cadadc9b..242ceea365827 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -12,6 +12,8 @@ import ( "github.com/google/uuid" "golang.org/x/xerrors" + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" @@ -200,6 +202,19 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) { return } + //nolint:gocritic // Neded to create first user. + if _, err := api.Database.UpdateUserProfile(dbauthz.AsSystemRestricted(ctx), database.UpdateUserProfileParams{ + ID: user.ID, + UpdatedAt: dbtime.Now(), + Email: user.Email, + Username: user.Username, + AvatarURL: user.AvatarURL, + Name: createUser.Name, + }); err != nil { + // This should not be a fatal error. Updating the user's profile can be done separately. + api.Logger.Error(ctx, "failed to update userprofile.Name", slog.Error(err)) + } + if api.RefreshEntitlements != nil { err = api.RefreshEntitlements(ctx) if err != nil { diff --git a/coderd/users_test.go b/coderd/users_test.go index 0fa42c4578c6d..ad337a064bea7 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -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") }) } } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index eb76239b84658..5d99e56820aa1 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -728,7 +728,7 @@ func TestWorkspaceDeleteSuspendedUser(t *testing.T) { validateCalls++ if userSuspended { // Simulate the user being suspended from the IDP too. - return "", http.StatusForbidden, fmt.Errorf("user is suspended") + return "", http.StatusForbidden, xerrors.New("user is suspended") } return "OK", 0, nil }, diff --git a/codersdk/users.go b/codersdk/users.go index 003ede2f9bd60..e478bfbb63a71 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -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"` diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 65b6e0fb47106..9536acdabaecf 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1325,6 +1325,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ```json { "email": "string", + "name": "string", "password": "string", "trial": true, "trial_info": { @@ -1345,6 +1346,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 | | | diff --git a/docs/api/users.md b/docs/api/users.md index db5f959a7fa76..67e9f7fdd4eb2 100644 --- a/docs/api/users.md +++ b/docs/api/users.md @@ -229,6 +229,7 @@ curl -X POST http://coder-server:8080/api/v2/users/first \ ```json { "email": "string", + "name": "string", "password": "string", "trial": true, "trial_info": { diff --git a/docs/cli/login.md b/docs/cli/login.md index 8dab8a884149c..a1dfa71f97f60 100644 --- a/docs/cli/login.md +++ b/docs/cli/login.md @@ -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-name + +| | | +| ----------- | ---------------------------------- | +| Type | string | +| Environment | $CODER_FIRST_USER_AME | + +Specifies a human-readable name for the first user of the deployment. + ### --first-user-password | | | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 86b6cf1e830f4..f89486f78ddca 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -193,6 +193,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; diff --git a/site/src/pages/SetupPage/SetupPageView.tsx b/site/src/pages/SetupPage/SetupPageView.tsx index af673acacc333..ee9b2bab705ac 100644 --- a/site/src/pages/SetupPage/SetupPageView.tsx +++ b/site/src/pages/SetupPage/SetupPageView.tsx @@ -23,6 +23,8 @@ import { countries } from "./countries"; export const Language = { emailLabel: "Email", passwordLabel: "Password", + nameLabel: "Name", + nameHelperText: 'Optional human-readable name', usernameLabel: "Username", emailInvalid: "Please enter a valid email address.", emailRequired: "Please enter an email address.", @@ -93,6 +95,7 @@ export const SetupPageView: FC = ({ email: "", password: "", username: "", + name: "", trial: false, trial_info: { first_name: "", @@ -164,7 +167,18 @@ export const SetupPageView: FC = ({ label={Language.passwordLabel} type="password" /> - + { + e.target.value = e.target.value.trim(); + form.handleChange(e); + }} + autoComplete="name" + fullWidth + label={Language.nameLabel} + helperText={Language.nameHelperText} + />