Skip to content

Commit

Permalink
[KYUUBI #6079] Web UI support Basic authN
Browse files Browse the repository at this point in the history
  • Loading branch information
wangjunbo committed Apr 10, 2024
1 parent c8a40d9 commit 5560d4f
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 23 deletions.
1 change: 1 addition & 0 deletions kyuubi-server/web-ui/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</script>

<template>
<login-modal />
<router-view />
</template>

Expand Down
88 changes: 88 additions & 0 deletions kyuubi-server/web-ui/src/components/login/LoginModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<template>
<el-dialog v-model="dialogVisible" :close-on-click-modal="false" width="600px">

Check failure on line 2 in kyuubi-server/web-ui/src/components/login/LoginModal.vue

View workflow job for this annotation

GitHub Actions / Style check (-Pflink-provided,hive-provided,spark-provided,spark-3.5,spark-3.4,spark-3.3,spark-3....

Replace `·v-model="dialogVisible"·:close-on-click-modal="false"` with `⏎····v-model="dialogVisible"⏎····:close-on-click-modal="false"⏎···`
<div class="dialog-header">
<img class="logo" src="@/assets/images/kyuubi-logo.svg" />
</div>
<el-form class="login-form">
<el-form-item label="Username">
<el-input v-model="username" placeholder="Username" />
</el-form-item>
<el-form-item label="Password">
<el-input type="password" v-model="password" placeholder="Password" />

Check warning on line 11 in kyuubi-server/web-ui/src/components/login/LoginModal.vue

View workflow job for this annotation

GitHub Actions / Style check (-Pflink-provided,hive-provided,spark-provided,spark-3.5,spark-3.4,spark-3.3,spark-3....

Attribute "v-model" should go before "type"
</el-form-item>
<el-form-item>
<p v-if="loginError" class="login-error">{{ loginError }}</p>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">

Check failure on line 17 in kyuubi-server/web-ui/src/components/login/LoginModal.vue

View workflow job for this annotation

GitHub Actions / Style check (-Pflink-provided,hive-provided,spark-provided,spark-3.5,spark-3.4,spark-3.3,spark-3....

`slot` attributes are deprecated
<el-button @click="closeDialog">Cancel</el-button>
<el-button type="primary" @click="handleLogin">Login</el-button>
</div>
</el-dialog>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';

Check failure on line 25 in kyuubi-server/web-ui/src/components/login/LoginModal.vue

View workflow job for this annotation

GitHub Actions / Style check (-Pflink-provided,hive-provided,spark-provided,spark-3.5,spark-3.4,spark-3.3,spark-3....

Replace `import·{·ref,·onMounted·}·from·'vue';` with `··import·{·ref,·onMounted·}·from·'vue'`
import { useAuthStore } from '@/pinia/auth/auth';

Check failure on line 26 in kyuubi-server/web-ui/src/components/login/LoginModal.vue

View workflow job for this annotation

GitHub Actions / Style check (-Pflink-provided,hive-provided,spark-provided,spark-3.5,spark-3.4,spark-3.3,spark-3....

Replace `import·{·useAuthStore·}·from·'@/pinia/auth/auth';` with `··import·{·useAuthStore·}·from·'@/pinia/auth/auth'`
const authStore = useAuthStore();

Check failure on line 28 in kyuubi-server/web-ui/src/components/login/LoginModal.vue

View workflow job for this annotation

GitHub Actions / Style check (-Pflink-provided,hive-provided,spark-provided,spark-3.5,spark-3.4,spark-3.3,spark-3....

Replace `const·authStore·=·useAuthStore();` with `··const·authStore·=·useAuthStore()`
const dialogVisible = ref(false);

Check failure on line 29 in kyuubi-server/web-ui/src/components/login/LoginModal.vue

View workflow job for this annotation

GitHub Actions / Style check (-Pflink-provided,hive-provided,spark-provided,spark-3.5,spark-3.4,spark-3.3,spark-3....

Replace `const·dialogVisible·=·ref(false);` with `··const·dialogVisible·=·ref(false)`
const username = ref('');

Check failure on line 30 in kyuubi-server/web-ui/src/components/login/LoginModal.vue

View workflow job for this annotation

GitHub Actions / Style check (-Pflink-provided,hive-provided,spark-provided,spark-3.5,spark-3.4,spark-3.3,spark-3....

Replace `const·username·=·ref('');` with `··const·username·=·ref('')`
const password = ref('');

Check failure on line 31 in kyuubi-server/web-ui/src/components/login/LoginModal.vue

View workflow job for this annotation

GitHub Actions / Style check (-Pflink-provided,hive-provided,spark-provided,spark-3.5,spark-3.4,spark-3.3,spark-3....

Replace `const·password·=·ref('');` with `··const·password·=·ref('')`
const loginError = ref('');

Check failure on line 32 in kyuubi-server/web-ui/src/components/login/LoginModal.vue

View workflow job for this annotation

GitHub Actions / Style check (-Pflink-provided,hive-provided,spark-provided,spark-3.5,spark-3.4,spark-3.3,spark-3....

Replace `const·loginError·=·ref('');` with `··const·loginError·=·ref('')`
const handleLogin = async () => {

Check failure on line 34 in kyuubi-server/web-ui/src/components/login/LoginModal.vue

View workflow job for this annotation

GitHub Actions / Style check (-Pflink-provided,hive-provided,spark-provided,spark-3.5,spark-3.4,spark-3.3,spark-3....

Insert `··`
try {
await authStore.setUser(username.value, password.value);
dialogVisible.value = false;
loginError.value = '';
} catch (error) {
loginError.value = (error as Error).message;
}
};
const closeDialog = () => {
dialogVisible.value = false;
loginError.value = '';
};
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: 80px;
height: auto;
margin-bottom: 10px;
}
.login-form {
margin-bottom: 20px;
}
.login-error {
color: red;
margin-top: 10px;
text-align: center;
}
.dialog-footer {
text-align: right;
padding: 15px 20px;
&>button:not(:last-child) {
margin-right: 10px;
}
}
</style>
91 changes: 69 additions & 22 deletions kyuubi-server/web-ui/src/layout/components/header/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,44 @@

<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.native="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>

Expand All @@ -48,12 +65,16 @@
import { useLocales } from './use-locales'
import { LOCALES } from './types'
import { reactive } from 'vue'
import { ref, onUnmounted } from 'vue';
import { useAuthStore } from '@/pinia/auth/auth';
import LoginModal from '@/login/LoginModal.vue';

const locales = reactive(LOCALES)
const { changeLocale, currentLocale } = useLocales()
const store = useStore()
const { isCollapse } = storeToRefs(store)
const { changeCollapse } = store


function _changeCollapse() {
changeCollapse()
Expand All @@ -62,20 +83,46 @@
function handleClick(command: string) {
changeLocale(command)
}

const authStore = useAuthStore();
const handleLogout = (command: string) => {
logout();
};
const logout = () => {
authStore.clearUser();
};

const showLoginModal = () => {
window.dispatchEvent(new CustomEvent('auth-required'));
};
</script>

<style lang="scss" scoped>
.header-container {
display: flex;
justify-content: space-between;
width: 100%;
}

> .el-icon {
.left-container {
>.el-icon {
padding: 0 24px;
cursor: pointer;
position: relative;
top: 2px;
}
}

.right-container {
display: flex;
align-items: center;

> .el-dropdown .el-icon {
>*:not(:last-child) {
margin-right: 16px;
}

>.el-dropdown .el-icon,
>.el-button {
position: relative;
top: 2px;
}
Expand Down
5 changes: 4 additions & 1 deletion kyuubi-server/web-ui/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ 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/LoginModal.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')
42 changes: 42 additions & 0 deletions kyuubi-server/web-ui/src/pinia/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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) {
try {
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');
}
} catch (error) {
throw error;
}
},
clearUser() {
this.user = null;
this.authToken = null;
this.isAuthenticated = false;
},
},
persist: {
key: 'auth',
},
});
8 changes: 8 additions & 0 deletions kyuubi-server/web-ui/src/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import axios, { AxiosResponse } from 'axios'
import { useAuthStore } from '@/pinia/auth/auth';

// create an axios instance
const service = axios.create({
Expand All @@ -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) => {
Expand Down Expand Up @@ -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)
}
)
Expand Down

0 comments on commit 5560d4f

Please sign in to comment.