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/6] 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/6] 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"
/>
From 349922d74a8328b117e28a9b87cb2dbeb4882243 Mon Sep 17 00:00:00 2001
From: Your Name <5115845+dauglyon@users.noreply.github.com>
Date: Mon, 1 Jun 2026 09:17:07 -0700
Subject: [PATCH 3/6] Skip user username pre-fill on ORCID signup
ORCID's provider-supplied username is the numeric ORCID iD (e.g.
0000-0002-1825-0097). auth2's NewUserName.sanitizeName strips all of
that to empty and getAvailableUserName falls back to user, so every
ORCID signup landed on the form with user1 (or user2, ...) already in
the username field.
Leave the username blank when the provider is OrcID so the user picks
their own. Display name and email pre-fill are unchanged.
---
src/features/signup/SignupSlice.test.tsx | 43 ++++++++++++++++++++++++
src/features/signup/SignupSlice.tsx | 13 +++++--
2 files changed, 53 insertions(+), 3 deletions(-)
create mode 100644 src/features/signup/SignupSlice.test.tsx
diff --git a/src/features/signup/SignupSlice.test.tsx b/src/features/signup/SignupSlice.test.tsx
new file mode 100644
index 00000000..9762bf9d
--- /dev/null
+++ b/src/features/signup/SignupSlice.test.tsx
@@ -0,0 +1,43 @@
+import { createTestStore } from '../../app/store';
+import { setLoginData } from './SignupSlice';
+
+const makeLoginData = (
+ provider: string,
+ availablename: string
+): Parameters[0] => ({
+ creationallowed: true,
+ expires: 0,
+ login: [],
+ provider,
+ create: [
+ {
+ provemail: 'jane@example.com',
+ provfullname: 'Jane Doe',
+ availablename,
+ id: '123',
+ provusername: '0000-0002-1825-0097',
+ },
+ ],
+});
+
+describe('signup setLoginData', () => {
+ test('pre-fills username from availablename for non-ORCID providers', () => {
+ const store = createTestStore();
+ store.dispatch(setLoginData(makeLoginData('Google', 'janedoe')));
+ expect(store.getState().signup.account.username).toBe('janedoe');
+ });
+
+ test('leaves username blank for ORCID logins to avoid user default', () => {
+ const store = createTestStore();
+ store.dispatch(setLoginData(makeLoginData('OrcID', 'user1')));
+ expect(store.getState().signup.account.username).toBeUndefined();
+ });
+
+ test('still pre-fills display name and email for ORCID', () => {
+ const store = createTestStore();
+ store.dispatch(setLoginData(makeLoginData('OrcID', 'user1')));
+ const account = store.getState().signup.account;
+ expect(account.display).toBe('Jane Doe');
+ expect(account.email).toBe('jane@example.com');
+ });
+});
diff --git a/src/features/signup/SignupSlice.tsx b/src/features/signup/SignupSlice.tsx
index 7c398b45..ec5d6fbd 100644
--- a/src/features/signup/SignupSlice.tsx
+++ b/src/features/signup/SignupSlice.tsx
@@ -44,9 +44,16 @@ export const signupSlice = createSlice({
// Set provider creeation data
state.loginData = action.payload;
// Set account defaults from provider
- state.account.display = action.payload?.create[0].provfullname;
- state.account.email = action.payload?.create[0].provemail;
- state.account.username = action.payload?.create[0].availablename;
+ const detail = action.payload?.create[0];
+ state.account.display = detail?.provfullname;
+ state.account.email = detail?.provemail;
+ // ORCID's provusername is the numeric ORCID iD, which auth2 cannot
+ // sanitize into a valid username and falls back to user. Leave the
+ // field blank for ORCID so the user picks their own.
+ state.account.username =
+ action.payload?.provider === 'OrcID'
+ ? undefined
+ : detail?.availablename;
},
setAccount: (
state,
From 171749b5ef8bab50edd40d2fbf4d0d1ce3e21808 Mon Sep 17 00:00:00 2001
From: Your Name <5115845+dauglyon@users.noreply.github.com>
Date: Thu, 10 Sep 2026 09:54:37 -0700
Subject: [PATCH 4/6] Rename the CDM sidebar link and redirect to BERDL
The link goes to hub.berdl.kbase.us, the BERDL JupyterHub. Rename the nav
label, route, component, deploy config key, and build env var to match.
The CI hub's login page (kbase/cdm-jupyterhub) still links to
/cdm/redirect, so that path redirects to the new route.
---
config.json | 4 ++--
scripts/build_deploy.ts | 6 +++---
src/app/Routes.tsx | 17 +++++++++++++----
.../CDMRedirect.tsx => berdl/BERDLRedirect.tsx} | 6 +++---
src/features/layout/LeftNavBar.tsx | 4 ++--
5 files changed, 23 insertions(+), 14 deletions(-)
rename src/features/{cdm/CDMRedirect.tsx => berdl/BERDLRedirect.tsx} (82%)
diff --git a/config.json b/config.json
index 70b54adb..eae5f2c6 100644
--- a/config.json
+++ b/config.json
@@ -10,7 +10,7 @@
"domain": "ci.kbase.us",
"legacy": "legacy.ci.kbase.us",
"public_url": "/",
- "cdm_domain": "cdmhub.ci.kbase.us"
+ "berdl_domain": "cdmhub.ci.kbase.us"
},
"ci-europa": {
@@ -47,7 +47,7 @@
"name": "kbase_session_backup",
"domain": ".kbase.us"
},
- "cdm_domain": "hub.berdl.kbase.us",
+ "berdl_domain": "hub.berdl.kbase.us",
"redirect_whitelist": ["*.berdl.kbase.us"]
}
}
diff --git a/scripts/build_deploy.ts b/scripts/build_deploy.ts
index 623e17ae..65d1bd5a 100755
--- a/scripts/build_deploy.ts
+++ b/scripts/build_deploy.ts
@@ -18,7 +18,7 @@ interface EnvironmentConfig {
name: string;
domain: string;
};
- cdm_domain?: string;
+ berdl_domain?: string;
redirect_whitelist?: string[];
}
@@ -54,7 +54,7 @@ const setEnvironment = (
legacy,
public_url: publicURL,
backup_cookie: backupCookie,
- cdm_domain: cdmDomain,
+ berdl_domain: berdlDomain,
redirect_whitelist: redirectWhitelist,
} = environmentConfig;
@@ -66,7 +66,7 @@ const setEnvironment = (
REACT_APP_KBASE_LEGACY_DOMAIN: legacy,
REACT_APP_KBASE_BACKUP_COOKIE_NAME: backupCookie?.name || '',
REACT_APP_KBASE_BACKUP_COOKIE_DOMAIN: backupCookie?.domain || '',
- REACT_APP_KBASE_CDM_DOMAIN: cdmDomain || 'cdmhub.' + domain,
+ REACT_APP_KBASE_BERDL_DOMAIN: berdlDomain || 'cdmhub.' + domain,
REACT_APP_REDIRECT_WHITELIST: redirectWhitelist?.join(',') || '',
};
Object.assign(process.env, envsNew);
diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx
index ccbfd694..a959c53f 100644
--- a/src/app/Routes.tsx
+++ b/src/app/Routes.tsx
@@ -38,7 +38,7 @@ import { LogInSessions } from '../features/account/LogInSessions';
import { UseAgreements } from '../features/account/UseAgreements';
import { skipToken } from '@reduxjs/toolkit/dist/query';
import { getMe } from '../common/api/authService';
-import { CDMRedirect } from '../features/cdm/CDMRedirect';
+import { BERDLRedirect } from '../features/berdl/BERDLRedirect';
import {
OrcidLink,
OrcidLinkContinue,
@@ -147,10 +147,19 @@ const Routes: FC = () => {
} />
- {/* CDM */}
-
- } />} />
+ {/* BERDL */}
+
+ } />}
+ />
+ {/* The CI hub's login page (kbase/cdm-jupyterhub templates/login.html)
+ still sends users to the pre-rename route. */}
+ }
+ />
{/* IFrame Fallback Routes */}
diff --git a/src/features/cdm/CDMRedirect.tsx b/src/features/berdl/BERDLRedirect.tsx
similarity index 82%
rename from src/features/cdm/CDMRedirect.tsx
rename to src/features/berdl/BERDLRedirect.tsx
index 656c47e7..f017a3f3 100644
--- a/src/features/cdm/CDMRedirect.tsx
+++ b/src/features/berdl/BERDLRedirect.tsx
@@ -2,9 +2,9 @@ import { Container, Stack } from '@mui/system';
import { useEffect } from 'react';
import { Loader } from '../../common/components';
-export const CDMRedirect = () => {
+export const BERDLRedirect = () => {
useEffect(() => {
- window.location.href = `https://${process.env.REACT_APP_KBASE_CDM_DOMAIN}/hub`;
+ window.location.href = `https://${process.env.REACT_APP_KBASE_BERDL_DOMAIN}/hub`;
});
return (
@@ -16,7 +16,7 @@ export const CDMRedirect = () => {
justifyContent={'center'}
>
- Redirecting to CDM
+ Redirecting to BERDL
);
diff --git a/src/features/layout/LeftNavBar.tsx b/src/features/layout/LeftNavBar.tsx
index e6274be1..4ebb2cb0 100644
--- a/src/features/layout/LeftNavBar.tsx
+++ b/src/features/layout/LeftNavBar.tsx
@@ -67,8 +67,8 @@ const LeftNavBar: FC = () => {
badgeColor={'primary'}
/>
Date: Thu, 10 Sep 2026 10:02:48 -0700
Subject: [PATCH 5/6] Log in to GHCR with the workflow token instead of the
retired bot secret
kbase/.github#70 (2026-04-10) removed the kbase-bot GHCR_TOKEN from the org
and moved the shared build-push workflow to github.token. This repo's local
copy of that workflow still used the secret, so every build since has failed
at the login step before lint, tests, or the image build ran.
---
.github/workflows/reusable_build-push.yml | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/reusable_build-push.yml b/.github/workflows/reusable_build-push.yml
index d68782cd..3ba43911 100644
--- a/.github/workflows/reusable_build-push.yml
+++ b/.github/workflows/reusable_build-push.yml
@@ -14,6 +14,9 @@ on:
jobs:
build-push-image:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
steps:
- name: Check out GitHub Repo
uses: actions/checkout@v3
@@ -26,12 +29,11 @@ jobs:
uses: docker/setup-buildx-action@v2
- name: Login to GHCR
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
registry: ghcr.io
- # note that the calling workflow must set `secrets: inherit`
- username: '${{ secrets.GHCR_USERNAME }}'
- password: '${{ secrets.GHCR_TOKEN }}'
+ username: ${{ github.actor }}
+ password: ${{ github.token }}
- name: Set up Node.js
uses: actions/setup-node@v2
From 3eeee99d4c333c2a0709e77c594a4f0bba5758d0 Mon Sep 17 00:00:00 2001
From: Your Name <5115845+dauglyon@users.noreply.github.com>
Date: Thu, 10 Sep 2026 10:12:08 -0700
Subject: [PATCH 6/6] Name the hub link Lakehouse rather than BERDL
Lakehouse is the user-facing name for hub.berdl.kbase.us. Rename the nav
label, route, component, deploy config key, and build env var to match;
/cdm/redirect still redirects to the new route.
---
config.json | 4 ++--
scripts/build_deploy.ts | 6 +++---
src/app/Routes.tsx | 10 +++++-----
.../LakehouseRedirect.tsx} | 6 +++---
src/features/layout/LeftNavBar.tsx | 4 ++--
5 files changed, 15 insertions(+), 15 deletions(-)
rename src/features/{berdl/BERDLRedirect.tsx => lakehouse/LakehouseRedirect.tsx} (81%)
diff --git a/config.json b/config.json
index eae5f2c6..340d8365 100644
--- a/config.json
+++ b/config.json
@@ -10,7 +10,7 @@
"domain": "ci.kbase.us",
"legacy": "legacy.ci.kbase.us",
"public_url": "/",
- "berdl_domain": "cdmhub.ci.kbase.us"
+ "lakehouse_domain": "cdmhub.ci.kbase.us"
},
"ci-europa": {
@@ -47,7 +47,7 @@
"name": "kbase_session_backup",
"domain": ".kbase.us"
},
- "berdl_domain": "hub.berdl.kbase.us",
+ "lakehouse_domain": "hub.berdl.kbase.us",
"redirect_whitelist": ["*.berdl.kbase.us"]
}
}
diff --git a/scripts/build_deploy.ts b/scripts/build_deploy.ts
index 65d1bd5a..b7c56dca 100755
--- a/scripts/build_deploy.ts
+++ b/scripts/build_deploy.ts
@@ -18,7 +18,7 @@ interface EnvironmentConfig {
name: string;
domain: string;
};
- berdl_domain?: string;
+ lakehouse_domain?: string;
redirect_whitelist?: string[];
}
@@ -54,7 +54,7 @@ const setEnvironment = (
legacy,
public_url: publicURL,
backup_cookie: backupCookie,
- berdl_domain: berdlDomain,
+ lakehouse_domain: lakehouseDomain,
redirect_whitelist: redirectWhitelist,
} = environmentConfig;
@@ -66,7 +66,7 @@ const setEnvironment = (
REACT_APP_KBASE_LEGACY_DOMAIN: legacy,
REACT_APP_KBASE_BACKUP_COOKIE_NAME: backupCookie?.name || '',
REACT_APP_KBASE_BACKUP_COOKIE_DOMAIN: backupCookie?.domain || '',
- REACT_APP_KBASE_BERDL_DOMAIN: berdlDomain || 'cdmhub.' + domain,
+ REACT_APP_KBASE_LAKEHOUSE_DOMAIN: lakehouseDomain || 'cdmhub.' + domain,
REACT_APP_REDIRECT_WHITELIST: redirectWhitelist?.join(',') || '',
};
Object.assign(process.env, envsNew);
diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx
index a959c53f..40fb77d3 100644
--- a/src/app/Routes.tsx
+++ b/src/app/Routes.tsx
@@ -38,7 +38,7 @@ import { LogInSessions } from '../features/account/LogInSessions';
import { UseAgreements } from '../features/account/UseAgreements';
import { skipToken } from '@reduxjs/toolkit/dist/query';
import { getMe } from '../common/api/authService';
-import { BERDLRedirect } from '../features/berdl/BERDLRedirect';
+import { LakehouseRedirect } from '../features/lakehouse/LakehouseRedirect';
import {
OrcidLink,
OrcidLinkContinue,
@@ -147,18 +147,18 @@ const Routes: FC = () => {
} />
- {/* BERDL */}
-
+ {/* Lakehouse */}
+
} />}
+ element={} />}
/>
{/* The CI hub's login page (kbase/cdm-jupyterhub templates/login.html)
still sends users to the pre-rename route. */}
}
+ element={}
/>
{/* IFrame Fallback Routes */}
diff --git a/src/features/berdl/BERDLRedirect.tsx b/src/features/lakehouse/LakehouseRedirect.tsx
similarity index 81%
rename from src/features/berdl/BERDLRedirect.tsx
rename to src/features/lakehouse/LakehouseRedirect.tsx
index f017a3f3..027670b4 100644
--- a/src/features/berdl/BERDLRedirect.tsx
+++ b/src/features/lakehouse/LakehouseRedirect.tsx
@@ -2,9 +2,9 @@ import { Container, Stack } from '@mui/system';
import { useEffect } from 'react';
import { Loader } from '../../common/components';
-export const BERDLRedirect = () => {
+export const LakehouseRedirect = () => {
useEffect(() => {
- window.location.href = `https://${process.env.REACT_APP_KBASE_BERDL_DOMAIN}/hub`;
+ window.location.href = `https://${process.env.REACT_APP_KBASE_LAKEHOUSE_DOMAIN}/hub`;
});
return (
@@ -16,7 +16,7 @@ export const BERDLRedirect = () => {
justifyContent={'center'}
>
- Redirecting to BERDL
+ Redirecting to Lakehouse
);
diff --git a/src/features/layout/LeftNavBar.tsx b/src/features/layout/LeftNavBar.tsx
index 4ebb2cb0..f50c0923 100644
--- a/src/features/layout/LeftNavBar.tsx
+++ b/src/features/layout/LeftNavBar.tsx
@@ -67,8 +67,8 @@ const LeftNavBar: FC = () => {
badgeColor={'primary'}
/>