diff --git a/src/features/layout/LeftNavBar.tsx b/src/features/layout/LeftNavBar.tsx
index 32817209..e6274be1 100644
--- a/src/features/layout/LeftNavBar.tsx
+++ b/src/features/layout/LeftNavBar.tsx
@@ -72,7 +72,7 @@ const LeftNavBar: FC = () => {
icon={faDatabase}
badge={'alpha'}
badgeColor={'warning'}
- requiredRole="CDM_JUPYTERHUB_ADMIN"
+ requiredRole="BERDL_USER"
/>
diff --git a/src/features/signup/AccountInformation.test.tsx b/src/features/signup/AccountInformation.test.tsx
index d491669b..82911794 100644
--- a/src/features/signup/AccountInformation.test.tsx
+++ b/src/features/signup/AccountInformation.test.tsx
@@ -135,4 +135,72 @@ describe('AccountInformation', () => {
expect(mockNavigate).toHaveBeenCalledWith('/signup/3');
});
+
+ test.each([
+ ['uppercase letters', 'BadUser'],
+ ['a leading digit', '1baduser'],
+ ['a hyphen', 'bad-user'],
+ ['a period', 'bad.user'],
+ ['repeating underscores', 'bad__user'],
+ ['a trailing underscore', 'baduser_'],
+ ])(
+ 'blocks submission and shows format error for username with %s',
+ async (_label, badName) => {
+ const store = createTestStore();
+ store.dispatch(
+ setLoginData({
+ creationallowed: true,
+ expires: 0,
+ login: [],
+ provider: 'Google',
+ create: [
+ {
+ provemail: 'test@test.com',
+ provfullname: 'Test User',
+ availablename: 'testuser',
+ id: '123',
+ provusername: 'testuser',
+ },
+ ],
+ })
+ );
+ renderWithProviders(, { store });
+
+ await act(() => {
+ fireEvent.change(screen.getByRole('textbox', { name: /Full Name/i }), {
+ target: { value: 'Test User' },
+ });
+ });
+ await act(() => {
+ fireEvent.change(screen.getByRole('textbox', { name: /Email/i }), {
+ target: { value: 'test@test.com' },
+ });
+ });
+ await act(() => {
+ fireEvent.change(
+ screen.getByRole('textbox', { name: /KBase Username/i }),
+ { target: { value: badName } }
+ );
+ });
+ await act(() => {
+ fireEvent.change(
+ screen.getByRole('textbox', { name: /Organization/i }),
+ { target: { value: 'Test Org' } }
+ );
+ });
+ await act(() => {
+ fireEvent.change(screen.getByRole('textbox', { name: /Department/i }), {
+ target: { value: 'Test Dept' },
+ });
+ });
+ await act(() => {
+ fireEvent.submit(screen.getByTestId('accountinfoform'));
+ });
+
+ expect(
+ screen.getByText(/may contain only lowercase letters/i)
+ ).toBeInTheDocument();
+ expect(mockNavigate).not.toHaveBeenCalledWith('/signup/3');
+ }
+ );
});
diff --git a/src/features/signup/AccountInformation.tsx b/src/features/signup/AccountInformation.tsx
index 156bd49a..6aeeefaf 100644
--- a/src/features/signup/AccountInformation.tsx
+++ b/src/features/signup/AccountInformation.tsx
@@ -58,8 +58,14 @@ export const AccountInformation: FC<{}> = () => {
const [username, setUsername] = useState(account.username ?? '');
const userAvail = loginUsernameSuggest.useQuery(username);
const nameShort = username.length < 3;
- const nameAvail =
- userAvail.currentData?.availablename === username.toLowerCase();
+ const nameTooLong = username.length > 100;
+ // Mirrors backend rules in kbase/auth2 NewUserName: must start with a
+ // lowercase letter; only [a-z0-9_]; no repeating or trailing underscores.
+ const nameFormatValid =
+ /^[a-z][a-z0-9_]*$/.test(username) &&
+ !username.includes('__') &&
+ !username.endsWith('_');
+ const nameAvail = userAvail.currentData?.availablename === username;
const surveyQuestion = 'How did you hear about us? (select all that apply)';
const [optionalText, setOptionalText] = useState>({});
@@ -199,7 +205,11 @@ export const AccountInformation: FC<{}> = () => {
required: true,
onChange: (e) => setUsername(e.currentTarget.value),
validate: () =>
- !nameShort && !userAvail.isFetching && nameAvail,
+ !nameShort &&
+ !nameTooLong &&
+ nameFormatValid &&
+ !userAvail.isFetching &&
+ nameAvail,
})}
defaultValue={account.username}
helperText={
@@ -209,6 +219,24 @@ export const AccountInformation: FC<{}> = () => {
Username is too short.
+ ) : nameTooLong ? (
+
+ Username must be at most 100 characters.
+
+
+ ) : !nameFormatValid ? (
+
+ Username may contain only lowercase letters, digits, and
+ underscores, and must start with a letter. Underscores
+ cannot repeat or end the username.
+ {userAvail.currentData?.availablename ? (
+ <>
+ {' '}
+ Suggested: "{userAvail.currentData.availablename}".
+ >
+ ) : null}
+
+
) : !nameAvail && !userAvail.isFetching ? (
Username is not available. Suggested: "
@@ -224,7 +252,12 @@ export const AccountInformation: FC<{}> = () => {
>
}
- error={nameShort || (!userAvail.isFetching && !nameAvail)}
+ error={
+ nameShort ||
+ nameTooLong ||
+ !nameFormatValid ||
+ (!userAvail.isFetching && !nameAvail)
+ }
/>