From d1b28af94144c7b4ceb92a017418163e4499d72d Mon Sep 17 00:00:00 2001 From: Your Name <5115845+dauglyon@users.noreply.github.com> Date: Fri, 29 May 2026 15:32:06 -0700 Subject: [PATCH 1/2] Validate signup username against backend format rules The frontend availability check compared availablename to username.toLowerCase(), so a username like "John" passed validation but was then rejected by auth2 for the uppercase letter. Inputs with characters auth2 strips (dots, hyphens, etc.) failed with a misleading "Username is not available" message. Mirror the kbase/auth2 NewUserName rules on the form: must start with a lowercase letter, only [a-z0-9_], no repeating or trailing underscores, at most 100 chars. Show a specific error for format violations and only treat availability mismatches as collisions. --- .../signup/AccountInformation.test.tsx | 68 +++++++++++++++++++ src/features/signup/AccountInformation.tsx | 41 +++++++++-- 2 files changed, 105 insertions(+), 4 deletions(-) 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) + } /> From ec1804fd53fb3d7e99033dfd1bf1337811c28ece Mon Sep 17 00:00:00 2001 From: Your Name <5115845+dauglyon@users.noreply.github.com> Date: Fri, 29 May 2026 15:32:20 -0700 Subject: [PATCH 2/2] Gate CDM sidebar link on BERDL_USER role The CDM nav item was gated on CDM_JUPYTERHUB_ADMIN, so only admins saw the link. Per the BERDL platform docs, BERDL_USER is the role that gates access to the lakehouse; CDM_JUPYTERHUB_ADMIN is a separate admin role for approving access requests. --- src/features/layout/LeftNavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" />