mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: implement sign up with GitHub for the first user (#16629)
Second PR to address https://github.com/coder/coder/issues/16230. See the issue for more context and discussion. It adds a "Continue with GitHub" button to the `/setup` page, so the deployment's admin can sign up with it. It also removes the "Username" and "Full Name" fields to make signing up with email faster. In the email flow, the username is now auto-generated based on the email, and full name is left empty. <img width="1512" alt="Screenshot 2025-02-21 at 17 51 22" src="https://github.com/user-attachments/assets/e7c6986b-c05e-458b-bb01-c3aea3b74c0e" /> There's a separate, follow up issue to visually align the `/setup` page with the new design system: https://github.com/coder/coder/issues/16653
This commit is contained in:
+32
-1
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/cryptokeys"
|
||||
"github.com/coder/coder/v2/coderd/idpsync"
|
||||
"github.com/coder/coder/v2/coderd/jwtutils"
|
||||
"github.com/coder/coder/v2/coderd/telemetry"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/apikey"
|
||||
@@ -1054,6 +1055,10 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
|
||||
defer params.CommitAuditLogs()
|
||||
if err != nil {
|
||||
if httpErr := idpsync.IsHTTPError(err); httpErr != nil {
|
||||
// In the device flow, the error page is rendered client-side.
|
||||
if api.GithubOAuth2Config.DeviceFlowEnabled && httpErr.RenderStaticPage {
|
||||
httpErr.RenderStaticPage = false
|
||||
}
|
||||
httpErr.Write(rw, r)
|
||||
return
|
||||
}
|
||||
@@ -1634,7 +1639,17 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
|
||||
isConvertLoginType = true
|
||||
}
|
||||
|
||||
if user.ID == uuid.Nil && !params.AllowSignups {
|
||||
// nolint:gocritic // Getting user count is a system function.
|
||||
userCount, err := tx.GetUserCount(dbauthz.AsSystemRestricted(ctx))
|
||||
if err != nil {
|
||||
return xerrors.Errorf("unable to fetch user count: %w", err)
|
||||
}
|
||||
|
||||
// Allow the first user to sign up with OIDC, regardless of
|
||||
// whether signups are enabled or not.
|
||||
allowSignup := userCount == 0 || params.AllowSignups
|
||||
|
||||
if user.ID == uuid.Nil && !allowSignup {
|
||||
signupsDisabledText := "Please contact your Coder administrator to request access."
|
||||
if api.OIDCConfig != nil && api.OIDCConfig.SignupsDisabledText != "" {
|
||||
signupsDisabledText = render.HTMLFromMarkdown(api.OIDCConfig.SignupsDisabledText)
|
||||
@@ -1695,6 +1710,12 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
|
||||
return xerrors.Errorf("unable to fetch default organization: %w", err)
|
||||
}
|
||||
|
||||
rbacRoles := []string{}
|
||||
// If this is the first user, add the owner role.
|
||||
if userCount == 0 {
|
||||
rbacRoles = append(rbacRoles, rbac.RoleOwner().String())
|
||||
}
|
||||
|
||||
//nolint:gocritic
|
||||
user, err = api.CreateUser(dbauthz.AsSystemRestricted(ctx), tx, CreateUserRequest{
|
||||
CreateUserRequestWithOrgs: codersdk.CreateUserRequestWithOrgs{
|
||||
@@ -1709,10 +1730,20 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
|
||||
},
|
||||
LoginType: params.LoginType,
|
||||
accountCreatorName: "oauth",
|
||||
RBACRoles: rbacRoles,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create user: %w", err)
|
||||
}
|
||||
|
||||
if userCount == 0 {
|
||||
telemetryUser := telemetry.ConvertUser(user)
|
||||
// The email is not anonymized for the first user.
|
||||
telemetryUser.Email = &user.Email
|
||||
api.Telemetry.Report(&telemetry.Snapshot{
|
||||
Users: []telemetry.User{telemetryUser},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Activate dormant user on sign-in
|
||||
|
||||
+55
-9
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/atomic"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
@@ -254,11 +255,20 @@ func TestUserOAuth2Github(t *testing.T) {
|
||||
})
|
||||
t.Run("BlockSignups", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, ps := dbtestutil.NewDB(t)
|
||||
|
||||
id := atomic.NewInt64(100)
|
||||
login := atomic.NewString("testuser")
|
||||
email := atomic.NewString("testuser@coder.com")
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
Database: db,
|
||||
Pubsub: ps,
|
||||
GithubOAuth2Config: &coderd.GithubOAuth2Config{
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
AllowOrganizations: []string{"coder"},
|
||||
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
|
||||
ListOrganizationMemberships: func(_ context.Context, _ *http.Client) ([]*github.Membership, error) {
|
||||
return []*github.Membership{{
|
||||
State: &stateActive,
|
||||
Organization: &github.Organization{
|
||||
@@ -266,16 +276,19 @@ func TestUserOAuth2Github(t *testing.T) {
|
||||
},
|
||||
}}, nil
|
||||
},
|
||||
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
|
||||
AuthenticatedUser: func(_ context.Context, _ *http.Client) (*github.User, error) {
|
||||
id := id.Load()
|
||||
login := login.Load()
|
||||
return &github.User{
|
||||
ID: github.Int64(100),
|
||||
Login: github.String("testuser"),
|
||||
ID: &id,
|
||||
Login: &login,
|
||||
Name: github.String("The Right Honorable Sir Test McUser"),
|
||||
}, nil
|
||||
},
|
||||
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
|
||||
ListEmails: func(_ context.Context, _ *http.Client) ([]*github.UserEmail, error) {
|
||||
email := email.Load()
|
||||
return []*github.UserEmail{{
|
||||
Email: github.String("testuser@coder.com"),
|
||||
Email: &email,
|
||||
Verified: github.Bool(true),
|
||||
Primary: github.Bool(true),
|
||||
}}, nil
|
||||
@@ -283,8 +296,23 @@ func TestUserOAuth2Github(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
// The first user in a deployment with signups disabled will be allowed to sign up,
|
||||
// but all the other users will not.
|
||||
resp := oauth2Callback(t, client)
|
||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// nolint:gocritic // Unit test
|
||||
count, err := db.GetUserCount(dbauthz.AsSystemRestricted(ctx))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), count)
|
||||
|
||||
id.Store(101)
|
||||
email.Store("someotheruser@coder.com")
|
||||
login.Store("someotheruser")
|
||||
|
||||
resp = oauth2Callback(t, client)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
t.Run("MultiLoginNotAllowed", func(t *testing.T) {
|
||||
@@ -988,6 +1016,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IgnoreEmailVerified bool
|
||||
IgnoreUserInfo bool
|
||||
UseAccessToken bool
|
||||
PrecreateFirstUser bool
|
||||
}{
|
||||
{
|
||||
Name: "NoSub",
|
||||
@@ -1150,7 +1179,17 @@ func TestUserOIDC(t *testing.T) {
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
StatusCode: http.StatusForbidden,
|
||||
StatusCode: http.StatusForbidden,
|
||||
PrecreateFirstUser: true,
|
||||
},
|
||||
{
|
||||
Name: "FirstSignup",
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "kyle@kwc.io",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
StatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "UsernameFromEmail",
|
||||
@@ -1443,6 +1482,15 @@ func TestUserOIDC(t *testing.T) {
|
||||
})
|
||||
numLogs := len(auditor.AuditLogs())
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
if tc.PrecreateFirstUser {
|
||||
owner.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
|
||||
Email: "precreated@coder.com",
|
||||
Username: "precreated",
|
||||
Password: "SomeSecurePassword!",
|
||||
})
|
||||
}
|
||||
|
||||
client, resp := fake.AttemptLogin(t, owner, tc.IDTokenClaims)
|
||||
numLogs++ // add an audit log for login
|
||||
require.Equal(t, tc.StatusCode, resp.StatusCode)
|
||||
@@ -1450,8 +1498,6 @@ func TestUserOIDC(t *testing.T) {
|
||||
tc.AssertResponse(t, resp)
|
||||
}
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
if tc.AssertUser != nil {
|
||||
user, err := client.User(ctx, "me")
|
||||
require.NoError(t, err)
|
||||
|
||||
+18
-21
@@ -118,6 +118,8 @@ func (api *API) firstUser(rw http.ResponseWriter, r *http.Request) {
|
||||
// @Success 201 {object} codersdk.CreateFirstUserResponse
|
||||
// @Router /users/first [post]
|
||||
func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
||||
// The first user can also be created via oidc, so if making changes to the flow,
|
||||
// ensure that the oidc flow is also updated.
|
||||
ctx := r.Context()
|
||||
var createUser codersdk.CreateFirstUserRequest
|
||||
if !httpapi.Read(ctx, rw, r, &createUser) {
|
||||
@@ -198,6 +200,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
||||
OrganizationIDs: []uuid.UUID{defaultOrg.ID},
|
||||
},
|
||||
LoginType: database.LoginTypePassword,
|
||||
RBACRoles: []string{rbac.RoleOwner().String()},
|
||||
accountCreatorName: "coder",
|
||||
})
|
||||
if err != nil {
|
||||
@@ -225,23 +228,6 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
||||
Users: []telemetry.User{telemetryUser},
|
||||
})
|
||||
|
||||
// TODO: @emyrk this currently happens outside the database tx used to create
|
||||
// the user. Maybe I add this ability to grant roles in the createUser api
|
||||
// and add some rbac bypass when calling api functions this way??
|
||||
// Add the admin role to this first user.
|
||||
//nolint:gocritic // needed to create first user
|
||||
_, err = api.Database.UpdateUserRoles(dbauthz.AsSystemRestricted(ctx), database.UpdateUserRolesParams{
|
||||
GrantedRoles: []string{rbac.RoleOwner().String()},
|
||||
ID: user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error updating user's roles.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.CreateFirstUserResponse{
|
||||
UserID: user.ID,
|
||||
OrganizationID: defaultOrg.ID,
|
||||
@@ -1351,6 +1337,7 @@ type CreateUserRequest struct {
|
||||
LoginType database.LoginType
|
||||
SkipNotifications bool
|
||||
accountCreatorName string
|
||||
RBACRoles []string
|
||||
}
|
||||
|
||||
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, error) {
|
||||
@@ -1360,6 +1347,13 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
|
||||
return database.User{}, xerrors.Errorf("invalid username %q: %w", req.Username, usernameValid)
|
||||
}
|
||||
|
||||
// If the caller didn't specify rbac roles, default to
|
||||
// a member of the site.
|
||||
rbacRoles := []string{}
|
||||
if req.RBACRoles != nil {
|
||||
rbacRoles = req.RBACRoles
|
||||
}
|
||||
|
||||
var user database.User
|
||||
err := store.InTx(func(tx database.Store) error {
|
||||
orgRoles := make([]string, 0)
|
||||
@@ -1376,10 +1370,9 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
|
||||
CreatedAt: dbtime.Now(),
|
||||
UpdatedAt: dbtime.Now(),
|
||||
HashedPassword: []byte{},
|
||||
// All new users are defaulted to members of the site.
|
||||
RBACRoles: []string{},
|
||||
LoginType: req.LoginType,
|
||||
Status: status,
|
||||
RBACRoles: rbacRoles,
|
||||
LoginType: req.LoginType,
|
||||
Status: status,
|
||||
}
|
||||
// If a user signs up with OAuth, they can have no password!
|
||||
if req.Password != "" {
|
||||
@@ -1437,6 +1430,10 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
|
||||
}
|
||||
|
||||
for _, u := range userAdmins {
|
||||
if u.ID == user.ID {
|
||||
// If the new user is an admin, don't notify them about themselves.
|
||||
continue
|
||||
}
|
||||
if _, err := api.NotificationsEnqueuer.EnqueueWithData(
|
||||
// nolint:gocritic // Need notifier actor to enqueue notifications
|
||||
dbauthz.AsNotifier(ctx),
|
||||
|
||||
@@ -16,7 +16,6 @@ test("setup deployment", async ({ page }) => {
|
||||
}
|
||||
|
||||
// Setup first user
|
||||
await page.getByLabel(Language.usernameLabel).fill(users.admin.username);
|
||||
await page.getByLabel(Language.emailLabel).fill(users.admin.email);
|
||||
await page.getByLabel(Language.passwordLabel).fill(users.admin.password);
|
||||
await page.getByTestId("create").click();
|
||||
|
||||
@@ -13,7 +13,6 @@ import { SetupPage } from "./SetupPage";
|
||||
import { Language as PageViewLanguage } from "./SetupPageView";
|
||||
|
||||
const fillForm = async ({
|
||||
username = "someuser",
|
||||
email = "someone@coder.com",
|
||||
password = "password",
|
||||
}: {
|
||||
@@ -21,10 +20,8 @@ const fillForm = async ({
|
||||
email?: string;
|
||||
password?: string;
|
||||
} = {}) => {
|
||||
const usernameField = screen.getByLabelText(PageViewLanguage.usernameLabel);
|
||||
const emailField = screen.getByLabelText(PageViewLanguage.emailLabel);
|
||||
const passwordField = screen.getByLabelText(PageViewLanguage.passwordLabel);
|
||||
await userEvent.type(usernameField, username);
|
||||
await userEvent.type(emailField, email);
|
||||
await userEvent.type(passwordField, password);
|
||||
const submitButton = screen.getByRole("button", {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { buildInfo } from "api/queries/buildInfo";
|
||||
import { createFirstUser } from "api/queries/users";
|
||||
import { authMethods, createFirstUser } from "api/queries/users";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { useAuthContext } from "contexts/auth/AuthProvider";
|
||||
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
|
||||
@@ -19,6 +19,7 @@ export const SetupPage: FC = () => {
|
||||
isSignedIn,
|
||||
isSigningIn,
|
||||
} = useAuthContext();
|
||||
const authMethodsQuery = useQuery(authMethods());
|
||||
const createFirstUserMutation = useMutation(createFirstUser());
|
||||
const setupIsComplete = !isConfiguringTheFirstUser;
|
||||
const { metadata } = useEmbeddedMetadata();
|
||||
@@ -34,7 +35,7 @@ export const SetupPage: FC = () => {
|
||||
});
|
||||
}, [buildInfoQuery.data]);
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || authMethodsQuery.isLoading) {
|
||||
return <Loader fullscreen />;
|
||||
}
|
||||
|
||||
@@ -54,6 +55,7 @@ export const SetupPage: FC = () => {
|
||||
<title>{pageTitle("Set up your account")}</title>
|
||||
</Helmet>
|
||||
<SetupPageView
|
||||
authMethods={authMethodsQuery.data}
|
||||
isLoading={isSigningIn || createFirstUserMutation.isLoading}
|
||||
error={createFirstUserMutation.error}
|
||||
onSubmit={async (firstUser) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import GitHubIcon from "@mui/icons-material/GitHub";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import AlertTitle from "@mui/material/AlertTitle";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Button from "@mui/material/Button";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import Link from "@mui/material/Link";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
@@ -15,8 +17,7 @@ import { PasswordField } from "components/PasswordField/PasswordField";
|
||||
import { SignInLayout } from "components/SignInLayout/SignInLayout";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { type FormikContextType, useFormik } from "formik";
|
||||
import type { FC } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { type ChangeEvent, type FC, useCallback } from "react";
|
||||
import { docs } from "utils/docs";
|
||||
import {
|
||||
getFormHelpers,
|
||||
@@ -33,7 +34,8 @@ export const Language = {
|
||||
emailInvalid: "Please enter a valid email address.",
|
||||
emailRequired: "Please enter an email address.",
|
||||
passwordRequired: "Please enter a password.",
|
||||
create: "Create account",
|
||||
create: "Continue with email",
|
||||
githubCreate: "Continue with GitHub",
|
||||
welcomeMessage: <>Welcome to Coder</>,
|
||||
firstNameLabel: "First name",
|
||||
lastNameLabel: "Last name",
|
||||
@@ -50,13 +52,29 @@ export const Language = {
|
||||
developersRequired: "Please select the number of developers in your company.",
|
||||
};
|
||||
|
||||
const usernameValidator = nameValidator(Language.usernameLabel);
|
||||
const usernameFromEmail = (email: string): string => {
|
||||
try {
|
||||
const emailPrefix = email.split("@")[0];
|
||||
const username = emailPrefix.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
||||
usernameValidator.validateSync(username);
|
||||
return username;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"failed to automatically generate username, defaulting to 'admin'",
|
||||
error,
|
||||
);
|
||||
return "admin";
|
||||
}
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
email: Yup.string()
|
||||
.trim()
|
||||
.email(Language.emailInvalid)
|
||||
.required(Language.emailRequired),
|
||||
password: Yup.string().required(Language.passwordRequired),
|
||||
username: nameValidator(Language.usernameLabel),
|
||||
username: usernameValidator,
|
||||
trial: Yup.bool(),
|
||||
trial_info: Yup.object().when("trial", {
|
||||
is: true,
|
||||
@@ -81,16 +99,23 @@ const numberOfDevelopersOptions = [
|
||||
"2500+",
|
||||
];
|
||||
|
||||
const iconStyles = {
|
||||
width: 16,
|
||||
height: 16,
|
||||
};
|
||||
|
||||
export interface SetupPageViewProps {
|
||||
onSubmit: (firstUser: TypesGen.CreateFirstUserRequest) => void;
|
||||
error?: unknown;
|
||||
isLoading?: boolean;
|
||||
authMethods: TypesGen.AuthMethods | undefined;
|
||||
}
|
||||
|
||||
export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
onSubmit,
|
||||
error,
|
||||
isLoading,
|
||||
authMethods,
|
||||
}) => {
|
||||
const form: FormikContextType<TypesGen.CreateFirstUserRequest> =
|
||||
useFormik<TypesGen.CreateFirstUserRequest>({
|
||||
@@ -112,6 +137,10 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
},
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
// With validate on blur set to true, the form lights up red whenever
|
||||
// you click out of it. This is a bit jarring. We instead validate
|
||||
// on submit and change.
|
||||
validateOnBlur: false,
|
||||
});
|
||||
const getFieldHelpers = getFormHelpers<TypesGen.CreateFirstUserRequest>(
|
||||
form,
|
||||
@@ -142,23 +171,36 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
</header>
|
||||
<VerticalForm onSubmit={form.handleSubmit}>
|
||||
<FormFields>
|
||||
<TextField
|
||||
autoFocus
|
||||
{...getFieldHelpers("username")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
autoComplete="username"
|
||||
fullWidth
|
||||
label={Language.usernameLabel}
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
autoComplete="name"
|
||||
fullWidth
|
||||
label={Language.nameLabel}
|
||||
/>
|
||||
{authMethods?.github.enabled && (
|
||||
<>
|
||||
<Button
|
||||
fullWidth
|
||||
component="a"
|
||||
href="/api/v2/users/oauth2/github/callback"
|
||||
variant="contained"
|
||||
startIcon={<GitHubIcon css={iconStyles} />}
|
||||
type="submit"
|
||||
size="xlarge"
|
||||
>
|
||||
{Language.githubCreate}
|
||||
</Button>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-[1px] w-full bg-border" />
|
||||
<div className="shrink-0 text-xs uppercase text-content-secondary tracking-wider">
|
||||
or
|
||||
</div>
|
||||
<div className="h-[1px] w-full bg-border" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<TextField
|
||||
{...getFieldHelpers("email")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
onChange={(event) => {
|
||||
const email = event.target.value;
|
||||
const username = usernameFromEmail(email);
|
||||
form.setFieldValue("username", username);
|
||||
onChangeTrimmed(form)(event as ChangeEvent<HTMLInputElement>);
|
||||
}}
|
||||
autoComplete="email"
|
||||
fullWidth
|
||||
label={Language.emailLabel}
|
||||
@@ -340,9 +382,7 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
data-testid="create"
|
||||
size="large"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="xlarge"
|
||||
>
|
||||
{Language.create}
|
||||
</LoadingButton>
|
||||
|
||||
Reference in New Issue
Block a user