This is an automated email from the ASF dual-hosted git repository.
chengpan pushed a commit to branch branch-1.9
in repository https://gitbox.apache.org/repos/asf/kyuubi.git
The following commit(s) were added to refs/heads/branch-1.9 by this push:
new 8b4a649d3 [KYUUBI #6079] Web UI support Basic authN
8b4a649d3 is described below
commit 8b4a649d34036928ea8af36f5099d518ae9b985d
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">


## 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]>
(cherry picked from commit 8bfae023086e78411132e876f7595536af1cdbee)
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)
}
)