This is an automated email from the ASF dual-hosted git repository.

LauraXia123 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 840fe67f55 Web UI support for built-in IdP Basic login (#11812)
840fe67f55 is described below

commit 840fe67f552ab93dcd64f4f0c4105ffa1e8c873d
Author: Octavio Herrera Contreras <[email protected]>
AuthorDate: Fri Jul 17 03:20:35 2026 -0700

    Web UI support for built-in IdP Basic login (#11812)
    
    Web UI support for built-in IdP Basic login [#11682] EPIC
    
    ### What changes were proposed in this pull request?
    - Fix: #11681: Added Basic login functionality following front-end
    session logic as requested + supporting UI modifications.
    
    ### Files Changed + why are the changes needed?
    
    1.
    
server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
    (Feature)
    - Adds web login header check to skip the unwanted browser basic auth
    popup
    
    2. web/web/src/app/login/components/BasicLogin.js (New Component)
    - Basic Login Page setup
    - Handles user login input + error catching and message upon invalid
    credentials
    - (Possibly add a more descriptive error message for invalid login?)
    - Prompts UI error popup upon failed login
    
    3. web/web/src/app/login/page.js (Feature)
    - When visited + logged in (confirmed with an access token) immediately
    redirects to metalakes
    - Replaced if-else logic with switch case to handle different login
    components (oidc, basic, and default)
    
    4. web/web/src/app/page.js (Feature)
    - Replaced empty web page with auto redirect to real main page
    /metalakes as described to be the home page in the issue thread.
    
    5. web/web/src/lib/api/auth/index.js (API Token)
    - Added new 'basicToken'
    - Allows simple token-accept functionalities, mainly
    'X-Gravitino-Web-Login' which is useful later on for stopping unwanted
    page refresh upon invalid logins
    
    6. web/web/src/lib/auth/providers/factory.js (Feature)
    - Added new variable 'authenticators' to read from the current config
    file to allow for setting 'providerType' and returning early before
    unsuitable OAuth behaviour, which isn't wanted for Basic Auth
    - Added missing 'this.providerType = providerType' line
    - Added null catch case for provider now that basic doesn't carry one
    - Fixed getProvider type to prevent possible null issues
    
    7. web/web/src/lib/provider/session.js (Feature)
    - Added else-if to redirect user to the metalake (home) page if already
    logged in otherwise forced into the login page.
    
    8. web/web/src/lib/store/auth/index.js (Feature)
    - Added basicLoginAction to replicate loginAction with custom Basic Auth
    logic
    - Added specific variable retention during refresh in authSlice.
    Prevents logout button from disappearing during refresh
    - Added authType to extraReducers for extra security with 'basic'
    functionality (previously only for oauth)
    
    9. web/web/src/lib/utils/axios/index.js (Feature)
    - Added isFullAuthHeader to set tokens relative to Basic Login auth
    while also maintaining jwt flow in the subsequent line
    - Added 'isFailedWebUILoginRequest' to prevent page refresh upon failed
    logins (this is a feature meant for OAuth which should be disabled for
    basic auth)
    - Added catch for 401 API pop-up caused by failed logins
    
    ### Does this PR introduce _any_ user-facing change?
    
    (Please list the user-facing changes introduced by your change,
    including
      1. Change in user-facing APIs
    - Added new basicLoginApi with basicToken.
    - Contains X-Gravitino-Web-Login
    
    
    ### How was this patch tested?
    
    (Please test your changes, and provide instructions on how to test it:
    1. Enabled basic auth mode using given instructions, and manually tested
      3. Ensured additions didn't cause any new unit test errors
    4. Manually reverted to the default conf file to ensure default
    behaviours unaffected
    
    ---------
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    Co-authored-by: Qian Xia <[email protected]>
---
 .../apache/gravitino/idp/IdpUserGroupManager.java  |   2 +-
 .../authentication/AuthenticationFilter.java       |   4 +-
 web-v2/web/src/app/login/components/BasicLogin.js  | 103 +++++++++++++++++++++
 .../web/src/app/login/components/DefaultLogin.js   |  78 ++++++----------
 web-v2/web/src/app/login/components/SimpleLogin.js |  55 +++++++++++
 web-v2/web/src/app/login/page.js                   |  48 ++++++++--
 web-v2/web/src/lib/api/auth/index.js               |  14 +++
 web-v2/web/src/lib/provider/session.js             |   8 ++
 web-v2/web/src/lib/store/auth/index.js             |  71 ++++++++++----
 web-v2/web/src/lib/utils/axios/index.js            |  30 +++++-
 10 files changed, 334 insertions(+), 79 deletions(-)

diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
index b0c2a5c68e..72e36d73f3 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
@@ -158,7 +158,7 @@ public class IdpUserGroupManager implements Closeable {
             "Invalid username or password", 
AuthConstants.AUTHORIZATION_BASIC_HEADER.trim());
       }
       return user;
-    } catch (NotFoundException e) {
+    } catch (Exception e) {
       throw new UnauthorizedException(
           "Invalid username or password", 
AuthConstants.AUTHORIZATION_BASIC_HEADER.trim());
     }
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
index bf4bbba784..e8b100af22 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
@@ -113,7 +113,9 @@ public class AuthenticationFilter implements Filter {
         // to let client to create correct authenticated request.
         // Refer to 
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate
         for (String challenge : ue.getChallenges()) {
-          resp.setHeader(AuthConstants.HTTP_CHALLENGE_HEADER, challenge);
+          if (!challenge.toLowerCase().startsWith("basic")) {
+            resp.setHeader(AuthConstants.HTTP_CHALLENGE_HEADER, challenge);
+          }
         }
       }
       sendAuthErrorResponse(resp, ue);
diff --git a/web-v2/web/src/app/login/components/BasicLogin.js 
b/web-v2/web/src/app/login/components/BasicLogin.js
new file mode 100644
index 0000000000..8de63746bc
--- /dev/null
+++ b/web-v2/web/src/app/login/components/BasicLogin.js
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+'use client'
+
+import { useRouter } from 'next/navigation'
+import { useState } from 'react'
+import { Button, Form, Input } from 'antd'
+import * as yup from 'yup'
+import { useForm, Controller } from 'react-hook-form'
+import { yupResolver } from '@hookform/resolvers/yup'
+
+import { useAppDispatch } from '@/lib/hooks/useStore'
+import { basicLoginAction } from '@/lib/store/auth'
+
+const defaultValues = {
+  username: '',
+  password: ''
+}
+
+const schema = yup.object().shape({
+  username: yup.string().required(),
+  password: yup.string().required()
+})
+
+function BasicLogin() {
+  const router = useRouter()
+  const dispatch = useAppDispatch()
+  const [isLoading, setIsLoading] = useState(false)
+
+  const {
+    control,
+    handleSubmit,
+    reset,
+    setError,
+    formState: { errors }
+  } = useForm({
+    defaultValues: Object.assign({}, defaultValues),
+    mode: 'onChange',
+    resolver: yupResolver(schema)
+  })
+
+  const onSubmit = async data => {
+    setIsLoading(true)
+    try {
+      // Using .unwrap() to catch failed login errors
+      await dispatch(basicLoginAction({ username: data.username, password: 
data.password, router })).unwrap()
+
+      // Clear the password after a successful login
+      reset({ username: data.username, password: '' })
+    } catch (error) {
+      setError('username', {
+        type: 'manual',
+        message: error?.message || 'Login failed'
+      })
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  const onError = () => {
+    // Form validation errors are handled by the UI
+  }
+
+  return (
+    <form autoComplete='off' onSubmit={handleSubmit(onSubmit, onError)}>
+      <Form component={false} layout='vertical'>
+        <Form.Item label='Username' validateStatus={errors.username ? 'error' 
: ''} help={errors.username?.message}>
+          <Controller name='username' control={control} render={({ field }) => 
<Input {...field} placeholder='' />} />
+        </Form.Item>
+
+        <Form.Item label='Password' validateStatus={errors.password ? 'error' 
: ''} help={errors.password?.message}>
+          <Controller
+            name='password'
+            control={control}
+            render={({ field }) => <Input.Password {...field} placeholder='' 
/>}
+          />
+        </Form.Item>
+
+        <Button block size='large' type='primary' htmlType='submit' 
loading={isLoading}>
+          Login
+        </Button>
+      </Form>
+    </form>
+  )
+}
+
+export default BasicLogin
diff --git a/web-v2/web/src/app/login/components/DefaultLogin.js 
b/web-v2/web/src/app/login/components/DefaultLogin.js
index a1df78df83..354b4fd5f2 100644
--- a/web-v2/web/src/app/login/components/DefaultLogin.js
+++ b/web-v2/web/src/app/login/components/DefaultLogin.js
@@ -39,13 +39,8 @@ function DefaultLogin() {
   }, [store.intervalId])
 
   const onFinish = async values => {
-    if (store.authType === 'simple' && store.anthEnable) {
-      await dispatch(setAuthUser({ name: values.username, type: 'user' }))
-      router.push('/metalakes')
-    } else {
-      await dispatch(loginAction({ params: values, router }))
-      await dispatch(setIntervalIdAction())
-    }
+    await dispatch(loginAction({ params: values, router }))
+    await dispatch(setIntervalIdAction())
   }
 
   return (
@@ -61,51 +56,36 @@ function DefaultLogin() {
         scope: ''
       }}
     >
-      {store.authType === 'simple' && store.anthEnable ? (
-        <>
-          <Form.Item label='Username' name='username' rules={[{ required: 
true, message: 'Username is required' }]}>
-            <Input placeholder='Please enter your username' />
-          </Form.Item>
-        </>
-      ) : (
-        <>
-          <Form.Item
-            label='Grant Type'
-            name='grant_type'
-            rules={[{ required: true, message: 'Grant Type is required' }]}
-            className='mt-4'
-          >
-            <Input disabled placeholder='Please enter the grant type' />
-          </Form.Item>
+      <Form.Item
+        label='Grant Type'
+        name='grant_type'
+        rules={[{ required: true, message: 'Grant Type is required' }]}
+        className='mt-4'
+      >
+        <Input disabled placeholder='Please enter the grant type' />
+      </Form.Item>
 
-          <Form.Item
-            label='Client ID'
-            name='client_id'
-            rules={[{ required: true, message: 'Client ID is required' }]}
-            className='mt-4'
-          >
-            <Input placeholder='' />
-          </Form.Item>
+      <Form.Item
+        label='Client ID'
+        name='client_id'
+        rules={[{ required: true, message: 'Client ID is required' }]}
+        className='mt-4'
+      >
+        <Input placeholder='' />
+      </Form.Item>
 
-          <Form.Item
-            label='Client Secret'
-            name='client_secret'
-            rules={[{ required: true, message: 'Client Secret is required' }]}
-            className='mt-4'
-          >
-            <Input placeholder='' />
-          </Form.Item>
+      <Form.Item
+        label='Client Secret'
+        name='client_secret'
+        rules={[{ required: true, message: 'Client Secret is required' }]}
+        className='mt-4'
+      >
+        <Input placeholder='' />
+      </Form.Item>
 
-          <Form.Item
-            label='Scope'
-            name='scope'
-            rules={[{ required: true, message: 'Scope is required' }]}
-            className='mt-4'
-          >
-            <Input placeholder='' />
-          </Form.Item>
-        </>
-      )}
+      <Form.Item label='Scope' name='scope' rules={[{ required: true, message: 
'Scope is required' }]} className='mt-4'>
+        <Input placeholder='' />
+      </Form.Item>
 
       <Form.Item className='mb-7 mt-12'>
         <Button type='primary' htmlType='submit' block size='large'>
diff --git a/web-v2/web/src/app/login/components/SimpleLogin.js 
b/web-v2/web/src/app/login/components/SimpleLogin.js
new file mode 100644
index 0000000000..10c94a737b
--- /dev/null
+++ b/web-v2/web/src/app/login/components/SimpleLogin.js
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use client'
+
+import { useRouter } from 'next/navigation'
+import { useEffect } from 'react'
+import { Button, Form, Input } from 'antd'
+
+import { useAppDispatch, useAppSelector } from '@/lib/hooks/useStore'
+import { clearIntervalId, setAuthUser } from '@/lib/store/auth'
+
+function SimpleLogin() {
+  const router = useRouter()
+  const dispatch = useAppDispatch()
+  const store = useAppSelector(state => state.auth)
+  const [form] = Form.useForm()
+
+  const onFinish = async values => {
+    await dispatch(setAuthUser({ name: values.username, type: 'user' }))
+    router.push('/metalakes')
+  }
+
+  return (
+    <Form form={form} layout='vertical' autoComplete='off' onFinish={onFinish}>
+      <Form.Item label='Username' name='username' rules={[{ required: true, 
message: 'Username is required' }]}>
+        <Input placeholder='Please enter your username' />
+      </Form.Item>
+
+      <Form.Item className='mb-7 mt-12'>
+        <Button type='primary' htmlType='submit' block size='large'>
+          Login
+        </Button>
+      </Form.Item>
+    </Form>
+  )
+}
+
+export default SimpleLogin
diff --git a/web-v2/web/src/app/login/page.js b/web-v2/web/src/app/login/page.js
index ea17bafcc2..0cb9dffa6b 100644
--- a/web-v2/web/src/app/login/page.js
+++ b/web-v2/web/src/app/login/page.js
@@ -22,15 +22,17 @@
 import Image from 'next/image'
 import { useSearchParams } from 'next/navigation'
 import { Roboto } from 'next/font/google'
-import { Alert, Card, Flex, Typography } from 'antd'
+import { Alert, Card, Flex, Typography, Spin } from 'antd'
 import { cn } from '@/lib/utils/tailwind'
 import { useEffect, useState, Suspense } from 'react'
 
 import OidcLogin from './components/OidcLogin'
 import DefaultLogin from './components/DefaultLogin'
+import BasicLogin from './components/BasicLogin'
+import SimpleLogin from './components/SimpleLogin'
 import { oauthProviderFactory } from '@/lib/auth/providers/factory'
 import { resetMetalakeStore } from '@/lib/store/metalakes'
-import { useAppDispatch } from '@/lib/hooks/useStore'
+import { useAppDispatch, useAppSelector } from '@/lib/hooks/useStore'
 
 const fonts = Roboto({ subsets: ['latin'], weight: ['400'], display: 'swap' })
 
@@ -42,22 +44,54 @@ const LoginContent = () => {
   const maxDurationReason = searchParams.get('reason') === 'max_duration'
   const [providerType, setProviderType] = useState(null)
   const dispatch = useAppDispatch()
+  const authType = useAppSelector(state => state.auth.authType)
 
   useEffect(() => {
+    dispatch(resetMetalakeStore())
+
+    if (authType !== 'oauth') {
+      return
+    }
+
     const detectProviderType = async () => {
       try {
         const detectedType = await oauthProviderFactory.getProviderType()
         setProviderType(detectedType)
       } catch (error) {
-        setProviderType('default') // fallback to default provider
+        setProviderType('default')
       }
     }
 
-    dispatch(resetMetalakeStore())
     detectProviderType()
-  }, [])
+  }, [authType, dispatch])
 
-  const useOidcLogin = providerType === 'oidc'
+  const renderLogin = () => {
+    switch (authType) {
+      case 'basic':
+        return <BasicLogin />
+
+      case 'simple':
+        return <SimpleLogin />
+
+      case 'oauth':
+        if (providerType === null)
+          return (
+            <div style={{ display: 'flex', justifyContent: 'center', padding: 
'2rem' }}>
+              <Spin />
+            </div>
+          )
+        if (providerType === 'oidc') return <OidcLogin />
+
+        return <DefaultLogin />
+
+      default:
+        return (
+          <div style={{ display: 'flex', justifyContent: 'center', padding: 
'2rem' }}>
+            <Spin />
+          </div>
+        )
+    }
+  }
 
   return (
     <Flex justify='center' align='center' style={{ minHeight: 'calc(100vh - 
7rem)' }}>
@@ -94,7 +128,7 @@ const LoginContent = () => {
           />
         )}
 
-        {useOidcLogin ? <OidcLogin /> : <DefaultLogin />}
+        {renderLogin()}
       </Card>
     </Flex>
   )
diff --git a/web-v2/web/src/lib/api/auth/index.js 
b/web-v2/web/src/lib/api/auth/index.js
index bace731fb9..7e22d23885 100644
--- a/web-v2/web/src/lib/api/auth/index.js
+++ b/web-v2/web/src/lib/api/auth/index.js
@@ -44,3 +44,17 @@ export const loginApi = (url, params) => {
     { withToken: false }
   )
 }
+
+export const basicLoginApi = basicToken => {
+  return defHttp.get(
+    {
+      url: '/api/version',
+      headers: {
+        Authorization: basicToken,
+        Accept: 'application/vnd.gravitino.v1+json',
+        'Content-Type': 'application/json'
+      }
+    },
+    { withToken: false }
+  )
+}
diff --git a/web-v2/web/src/lib/provider/session.js 
b/web-v2/web/src/lib/provider/session.js
index deab2c948c..c8f67f4ba2 100644
--- a/web-v2/web/src/lib/provider/session.js
+++ b/web-v2/web/src/lib/provider/session.js
@@ -100,6 +100,14 @@ const AuthProvider = ({ children }) => {
           dispatch(setAuthUser(sessionUser))
           goToMetalakeListPage()
         }
+      } else if (authType === 'basic') {
+        const tokenToUse = sessionStorage.getItem('accessToken')
+
+        if (tokenToUse) {
+          goToMetalakeListPage()
+        } else {
+          router.push('/login')
+        }
       } else if (authType === 'oauth') {
         // Clear any residual simpleAuthUser when authType is oauth
         sessionStorage.removeItem('simpleAuthUser')
diff --git a/web-v2/web/src/lib/store/auth/index.js 
b/web-v2/web/src/lib/store/auth/index.js
index a7aa56ed72..1931679ba2 100644
--- a/web-v2/web/src/lib/store/auth/index.js
+++ b/web-v2/web/src/lib/store/auth/index.js
@@ -22,7 +22,7 @@ import toast from 'react-hot-toast'
 
 import { to, isProdEnv } from '@/lib/utils'
 
-import { getAuthConfigsApi, loginApi } from '@/lib/api/auth'
+import { getAuthConfigsApi, loginApi, basicLoginApi } from '@/lib/api/auth'
 
 import { initialVersion } from '@/lib/store/sys'
 import { oauthProviderFactory } from '@/lib/auth/providers/factory'
@@ -98,6 +98,36 @@ export const loginAction = 
createAsyncThunk('auth/loginAction', async ({ params,
   return { token: access_token, expired: expires_in }
 })
 
+export const basicLoginAction = createAsyncThunk(
+  'auth/basicLoginAction',
+  async ({ username, password, router }, { dispatch }) => {
+    const basicToken = `Basic ${btoa(`${username}:${password}`)}`
+
+    const [err, res] = await to(basicLoginApi(basicToken))
+
+    if (err || !res) {
+      const message =
+        err?.response?.status === 401 ? 'Invalid username or password' : 
err?.response?.data?.err || err?.message
+
+      toast.error(message, {
+        id: `global_error_message_status_${err?.response?.status}`
+      })
+
+      throw new Error(message)
+    }
+
+    sessionStorage.setItem('accessToken', basicToken)
+    sessionStorage.setItem('isIdle', false)
+    sessionStorage.removeItem('expiredIn') // Basic auth does not have an 
expiration time
+
+    dispatch(setAuthToken(basicToken))
+    await dispatch(initialVersion())
+    router.push('/metalakes')
+
+    return { token: basicToken, expired: '' }
+  }
+)
+
 export const logoutAction = createAsyncThunk(
   'auth/logoutAction',
   async ({ router, reason }, { getState, dispatch }) => {
@@ -144,25 +174,29 @@ export const logoutAction = createAsyncThunk(
       } catch (error) {
         console.warn('[Logout Action] Provider cleanup failed:', error)
       }
+    }
 
-      // Clear legacy auth tokens
-      localStorage.removeItem('accessToken')
-      localStorage.removeItem('authParams')
-      localStorage.removeItem('expiredIn')
-      localStorage.removeItem('version')
+    // Clear legacy auth tokens (local and session storage) after provider 
cleanup
+    localStorage.removeItem('accessToken')
+    sessionStorage.removeItem('accessToken')
 
-      dispatch(clearIntervalId())
-      dispatch(setAuthToken(''))
-    }
+    localStorage.removeItem('authParams')
+    sessionStorage.removeItem('authParams')
+
+    localStorage.removeItem('expiredIn')
+    sessionStorage.removeItem('expiredIn')
+
+    localStorage.removeItem('version')
+    sessionStorage.removeItem('version')
+
+    dispatch(clearIntervalId())
+    dispatch(setAuthToken(''))
 
     // Always clear authUser in Redux and sessionStorage on logout
     // This ensures consistent behavior for both OAuth and simple auth
     dispatch(setAuthUser(null))
     sessionStorage.removeItem('simpleAuthToken')
 
-    // Clear persisted authType to avoid stale auth mode on next visit
-    localStorage.removeItem('authType')
-
     // Reset provider factory to ensure clean state for next login
     oauthProviderFactory.reset()
 
@@ -194,10 +228,15 @@ export const authSlice = createSlice({
   name: 'auth',
   initialState: {
     oauthUrl: null,
-    authType: null,
-    authToken: null,
-    authParams: null,
-    expiredIn: null,
+    authType: typeof window !== 'undefined' ? localStorage.getItem('authType') 
: null,
+    authToken:
+      typeof window !== 'undefined'
+        ? localStorage.getItem('authType') === 'basic'
+          ? sessionStorage.getItem('accessToken')
+          : localStorage.getItem('accessToken')
+        : null,
+    authParams: typeof window !== 'undefined' ? 
localStorage.getItem('authParams') : null,
+    expiredIn: typeof window !== 'undefined' ? 
localStorage.getItem('expiredIn') : null,
     intervalId: null,
     anthEnable: null,
     serviceAdmins: null,
diff --git a/web-v2/web/src/lib/utils/axios/index.js 
b/web-v2/web/src/lib/utils/axios/index.js
index 00e8c0fd02..1573a5c207 100644
--- a/web-v2/web/src/lib/utils/axios/index.js
+++ b/web-v2/web/src/lib/utils/axios/index.js
@@ -201,6 +201,15 @@ const transform = {
         if (user) {
           config.headers.Authorization = `Basic 
${Buffer.from(user).toString('base64')}`
         }
+      } else if (authType === 'basic') {
+        // Basic auth: use token from sessionStorage
+        const token = sessionStorage.getItem('accessToken')
+        if (token && config?.requestOptions?.withToken !== false) {
+          const isFullAuthHeader = token.startsWith('Basic ') || 
token.startsWith('Bearer ')
+
+          config.headers.Authorization =
+            isFullAuthHeader || !options.authenticationScheme ? token : 
`${options.authenticationScheme} ${token}`
+        }
       } else {
         // authType not yet persisted during bootstrap (before /configs 
resolves).
         // Fall back to persisted auth artifacts to avoid spurious 401s.
@@ -244,7 +253,7 @@ const transform = {
     }
 
     try {
-      if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
+      if (code === 'ECONNABORTED' && message?.includes('timeout')) {
         errMessage = 'The interface request timed out, please refresh the page 
and try again!'
       }
       if (err?.includes('Network Error')) {
@@ -264,12 +273,23 @@ const transform = {
       throw new Error(error)
     }
 
+    // During a failed basic login request, don't reload the page as this 
would prevent error message display.
+    const isBasicLoginRequest =
+      response?.status === 401 &&
+      originConfig?.url?.includes('/api/version') &&
+      originConfig?.requestOptions?.withToken === false
+
+    if (isBasicLoginRequest) {
+      return Promise.reject(error)
+    }
+
     checkStatus(error?.response?.status, msg, errorMessageMode)
 
-    if (response?.status === 401 && !originConfig._retry && 
response.config.url !== githubApis.GET) {
-      // Clear OAuth tokens
+    if (response?.status === 401 && !originConfig?._retry && 
response?.config?.url !== githubApis.GET) {
+      // Clear OAuth + BasicAuth tokens
       localStorage.removeItem('accessToken')
       localStorage.removeItem('authParams')
+      sessionStorage.removeItem('accessToken')
 
       // Clear simple auth data
       sessionStorage.removeItem('simpleAuthUser')
@@ -292,8 +312,8 @@ const transform = {
     }
 
     const retryRequest = new AxiosRetry()
-    const { isOpenRetry } = originConfig.requestOptions.retryRequest
-    originConfig.method?.toUpperCase() === RequestEnum.GET && isOpenRetry && 
retryRequest.retry(axiosInstance, error)
+    const { isOpenRetry } = originConfig?.requestOptions?.retryRequest
+    originConfig?.method?.toUpperCase() === RequestEnum.GET && isOpenRetry && 
retryRequest.retry(axiosInstance, error)
 
     return Promise.reject(error)
   }

Reply via email to