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

chengpan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kyuubi.git


The following commit(s) were added to refs/heads/master by this push:
     new 8bfae0230 [KYUUBI #6079] Web UI support Basic authN
8bfae0230 is described below

commit 8bfae023086e78411132e876f7595536af1cdbee
Author: wangjunbo <[email protected]>
AuthorDate: Thu Apr 11 19:26:45 2024 +0800

    [KYUUBI #6079] Web UI support Basic authN
    
    # :mag: Description
    ## Issue References ๐Ÿ”—
    
    This pull request fixes #6079
    
    ## Describe Your Solution ๐Ÿ”ง
    
    <img width="1160" alt="image" 
src="https://github.com/apache/kyuubi/assets/25627922/eef26a0e-478a-4e65-a0a4-629e0afa9a52";>
    
    
![zpHy0SGauD](https://github.com/apache/kyuubi/assets/25627922/176d4436-509b-406a-984b-8eb0dab9698e)
    
    
![image](https://github.com/apache/kyuubi/assets/25627922/3d637718-b177-48b2-bd6a-9ec8065b8b9c)
    
    ## Types of changes :bookmark:
    
    - [ ] Bugfix (non-breaking change which fixes an issue)
    - [x] New feature (non-breaking change which adds functionality)
    - [ ] Breaking change (fix or feature that would cause existing 
functionality to change)
    
    ## Test Plan ๐Ÿงช
    
    ---
    
    # Checklist ๐Ÿ“
    
    - [ ] This patch was not authored or co-authored using [Generative 
Tooling](https://www.apache.org/legal/generative-tooling.html)
    
    **Be nice. Be informative.**
    
    Closes #6258 from beryllw/kyuubi-6079.
    
    Closes #6079
    
    7a90286a7 [wangjunbo] support input valid
    6e2093e66 [wangjunbo] format code
    a781c0ae7 [wangjunbo] add License
    788bdfad0 [wangjunbo] [KYUUBI #6079] Web UI support Basic authN
    97772e595 [wangjunbo] [KYUUBI #6079] Web UI support Basic authN
    5560d4f6c [wangjunbo] [KYUUBI #6079] Web UI support Basic authN
    
    Authored-by: wangjunbo <[email protected]>
    Signed-off-by: Cheng Pan <[email protected]>
---
 kyuubi-server/web-ui/src/App.vue                   |   1 +
 .../web-ui/src/components/login/index.vue          | 111 +++++++++++++++++++++
 .../web-ui/src/layout/components/header/index.vue  |  91 +++++++++++++----
 kyuubi-server/web-ui/src/main.ts                   |  11 +-
 kyuubi-server/web-ui/src/pinia/auth/auth.ts        |  55 ++++++++++
 kyuubi-server/web-ui/src/utils/request.ts          |   8 ++
 6 files changed, 255 insertions(+), 22 deletions(-)

diff --git a/kyuubi-server/web-ui/src/App.vue b/kyuubi-server/web-ui/src/App.vue
index 76f5f08cd..5a4eeb9f0 100644
--- a/kyuubi-server/web-ui/src/App.vue
+++ b/kyuubi-server/web-ui/src/App.vue
@@ -21,6 +21,7 @@
 </script>
 
 <template>
+  <login-modal />
   <router-view />
 </template>
 
diff --git a/kyuubi-server/web-ui/src/components/login/index.vue 
b/kyuubi-server/web-ui/src/components/login/index.vue
new file mode 100644
index 000000000..fb3fbc3bf
--- /dev/null
+++ b/kyuubi-server/web-ui/src/components/login/index.vue
@@ -0,0 +1,111 @@
+<!--
+* 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.
+-->
+
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    :close-on-click-modal="false"
+    width="400px">
+    <div class="dialog-header">
+      <img class="logo" src="@/assets/images/kyuubi-logo.svg" />
+    </div>
+    <el-form class="login-form">
+      <el-form-item>
+        <el-input v-model="username" placeholder="Username" />
+      </el-form-item>
+      <el-form-item>
+        <el-input v-model="password" type="password" placeholder="Password" />
+      </el-form-item>
+      <el-form-item>
+        <p v-if="loginError" class="login-error">{{ loginError }}</p>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button
+          type="primary"
+          :disabled="isLoginDisabled"
+          @click="handleLogin"
+          >Log in</el-button
+        >
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed, onMounted } from 'vue'
+  import { useAuthStore } from '@/pinia/auth/auth'
+
+  const authStore = useAuthStore()
+  const dialogVisible = ref(false)
+  const username = ref('')
+  const password = ref('')
+  const loginError = ref('')
+
+  const isLoginDisabled = computed(() => {
+    return (
+      username.value.trim().length === 0 || password.value.trim().length === 0
+    )
+  })
+
+  const handleLogin = async () => {
+    try {
+      await authStore.setUser(username.value, password.value)
+      dialogVisible.value = false
+    } catch (error) {
+      loginError.value = (error as Error).message
+    }
+  }
+
+  onMounted(() => {
+    window.addEventListener('auth-required', () => {
+      dialogVisible.value = true
+    })
+  })
+</script>
+
+<style scoped>
+  .dialog-header {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    margin-bottom: 20px;
+  }
+
+  .logo {
+    width: 100px;
+    height: auto;
+    margin-bottom: 10px;
+  }
+
+  .login-form {
+    margin-bottom: 20px;
+  }
+
+  .login-error {
+    color: red;
+    margin-top: 10px;
+    text-align: left;
+  }
+
+  .dialog-footer {
+    text-align: center;
+    padding: 15px 20px;
+  }
+</style>
diff --git a/kyuubi-server/web-ui/src/layout/components/header/index.vue 
b/kyuubi-server/web-ui/src/layout/components/header/index.vue
index ada2a8605..d756d5877 100644
--- a/kyuubi-server/web-ui/src/layout/components/header/index.vue
+++ b/kyuubi-server/web-ui/src/layout/components/header/index.vue
@@ -18,27 +18,49 @@
 
 <template>
   <div class="header-container">
-    <el-icon :size="20" @click="_changeCollapse">
-      <component :is="isCollapse ? 'Expand' : 'Fold'" />
-    </el-icon>
-    <el-dropdown @command="handleClick">
-      <span class="el-dropdown-link">
-        {{ currentLocale }}
-        <el-icon class="el-icon--right">
-          <arrow-down />
-        </el-icon>
-      </span>
-      <template #dropdown>
-        <el-dropdown-menu>
-          <el-dropdown-item
-            v-for="(locale, key) in locales"
-            :key="key"
-            :command="locale.key">
-            {{ locale.label }}
-          </el-dropdown-item>
-        </el-dropdown-menu>
+    <div class="left-container">
+      <el-icon :size="20" @click="_changeCollapse">
+        <component :is="isCollapse ? 'Expand' : 'Fold'" />
+      </el-icon>
+    </div>
+    <div class="right-container">
+      <template v-if="authStore.isAuthenticated">
+        <el-dropdown>
+          <span class="el-dropdown-link">
+            {{ authStore.user }}
+            <el-icon class="el-icon--right">
+              <arrow-down />
+            </el-icon>
+          </span>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item @click="handleLogout"
+                >Sign out</el-dropdown-item
+              >
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
       </template>
-    </el-dropdown>
+      <el-button v-else @click="showLoginModal">Sign in</el-button>
+      <el-dropdown @command="handleClick">
+        <span class="el-dropdown-link">
+          {{ currentLocale }}
+          <el-icon class="el-icon--right">
+            <arrow-down />
+          </el-icon>
+        </span>
+        <template #dropdown>
+          <el-dropdown-menu>
+            <el-dropdown-item
+              v-for="(locale, key) in locales"
+              :key="key"
+              :command="locale.key">
+              {{ locale.label }}
+            </el-dropdown-item>
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+    </div>
   </div>
 </template>
 
@@ -48,6 +70,7 @@
   import { useLocales } from './use-locales'
   import { LOCALES } from './types'
   import { reactive } from 'vue'
+  import { useAuthStore } from '@/pinia/auth/auth'
 
   const locales = reactive(LOCALES)
   const { changeLocale, currentLocale } = useLocales()
@@ -62,6 +85,18 @@
   function handleClick(command: string) {
     changeLocale(command)
   }
+
+  const authStore = useAuthStore()
+  const handleLogout = () => {
+    logout()
+  }
+  const logout = () => {
+    authStore.clearUser()
+  }
+
+  const showLoginModal = () => {
+    window.dispatchEvent(new CustomEvent('auth-required'))
+  }
 </script>
 
 <style lang="scss" scoped>
@@ -69,13 +104,27 @@
     display: flex;
     justify-content: space-between;
     width: 100%;
+  }
 
+  .left-container {
     > .el-icon {
       padding: 0 24px;
       cursor: pointer;
+      position: relative;
+      top: 2px;
+    }
+  }
+
+  .right-container {
+    display: flex;
+    align-items: center;
+
+    > *:not(:last-child) {
+      margin-right: 16px;
     }
 
-    > .el-dropdown .el-icon {
+    > .el-dropdown .el-icon,
+    > .el-button {
       position: relative;
       top: 2px;
     }
diff --git a/kyuubi-server/web-ui/src/main.ts b/kyuubi-server/web-ui/src/main.ts
index 7e7d4d354..8268c9e45 100644
--- a/kyuubi-server/web-ui/src/main.ts
+++ b/kyuubi-server/web-ui/src/main.ts
@@ -24,9 +24,18 @@ import '@/assets/styles/element/index.scss'
 import '@/assets/styles/index.scss'
 import App from './App.vue'
 import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+import LoginModal from '@/components/login/index.vue'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
 
 const app = createApp(App)
 for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
   app.component(key, component)
 }
-app.use(router).use(store).use(i18n).use(ElementPlus).mount('#app')
+store.use(piniaPluginPersistedstate)
+app
+  .component('LoginModal', LoginModal)
+  .use(router)
+  .use(store)
+  .use(i18n)
+  .use(ElementPlus)
+  .mount('#app')
diff --git a/kyuubi-server/web-ui/src/pinia/auth/auth.ts 
b/kyuubi-server/web-ui/src/pinia/auth/auth.ts
new file mode 100644
index 000000000..1a5684619
--- /dev/null
+++ b/kyuubi-server/web-ui/src/pinia/auth/auth.ts
@@ -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.
+ */
+
+import { defineStore } from 'pinia'
+import request from '@/utils/request'
+
+export const useAuthStore = defineStore('auth', {
+  state: () => ({
+    user: null as string | null,
+    authToken: null as string | null,
+    isAuthenticated: false
+  }),
+  actions: {
+    async setUser(user: string, password: string) {
+      const response = await request({
+        url: 'api/v1/ping',
+        method: 'get',
+        auth: {
+          username: user,
+          password: password
+        }
+      })
+
+      if (response) {
+        this.user = user
+        this.authToken = `Basic ${btoa(user + ':' + password)}`
+        this.isAuthenticated = true
+      } else {
+        throw new Error('Authentication failed')
+      }
+    },
+    clearUser() {
+      this.user = null
+      this.authToken = null
+      this.isAuthenticated = false
+    }
+  },
+  persist: {
+    key: 'auth'
+  }
+})
diff --git a/kyuubi-server/web-ui/src/utils/request.ts 
b/kyuubi-server/web-ui/src/utils/request.ts
index 6171ed9cd..3b5b9c34b 100644
--- a/kyuubi-server/web-ui/src/utils/request.ts
+++ b/kyuubi-server/web-ui/src/utils/request.ts
@@ -16,6 +16,7 @@
  */
 
 import axios, { AxiosResponse } from 'axios'
+import { useAuthStore } from '@/pinia/auth/auth'
 
 // create an axios instance
 const service = axios.create({
@@ -28,6 +29,10 @@ const service = axios.create({
 service.interceptors.request.use(
   (config) => {
     // do something before request is sent
+    const authStore = useAuthStore()
+    if (authStore.isAuthenticated) {
+      config.headers.Authorization = authStore.authToken
+    }
     return config
   },
   (error) => {
@@ -56,6 +61,9 @@ service.interceptors.response.use(
   (error) => {
     // for debug
     // do something when error
+    if (error.response && error.response.status === 401) {
+      window.dispatchEvent(new CustomEvent('auth-required'))
+    }
     return Promise.reject(error)
   }
 )

Reply via email to