modify Login UI for OIDC login

This commit is contained in:
Tao Chen
2024-10-31 08:57:31 +08:00
parent 40a2d889ed
commit c165f54ce8
6 changed files with 338 additions and 78 deletions
+24
View File
@@ -0,0 +1,24 @@
import request from '@/utils/request';
export function loginOptions() {
return request({
url: '/login-options',
method: 'get',
})
}
export function oidcAuth (data) {
return request({
url: '/oidc/auth',
method: 'post',
data,
})
}
export function oidcQuery(params){
return request({
url: '/oidc/auth-query',
method: 'get',
params,
})
}
+50 -8
View File
@@ -1,8 +1,9 @@
import { defineStore, acceptHMRUpdate } from 'pinia' import { defineStore, acceptHMRUpdate } from 'pinia'
import { current, login } from '@/api/user' import { current, login } from '@/api/user'
import { setToken, removeToken } from '@/utils/auth' import { setToken, removeToken, setCode, removeCode } from '@/utils/auth'
import { useRouteStore } from '@/store/router' import { useRouteStore } from '@/store/router'
import { useAppStore } from '@/store/app' import { useAppStore } from '@/store/app'
import { oidcAuth, oidcQuery } from '@/api/login';
export const useUserStore = defineStore({ export const useUserStore = defineStore({
id: 'user', id: 'user',
@@ -16,19 +17,17 @@ export const useUserStore = defineStore({
}), }),
actions: { actions: {
logout () { logout() {
removeToken() removeToken()
removeCode()
this.$patch({ this.$patch({
name: '', name: '',
role: {}, role: {},
}) })
}, },
async login (form) { saveUserData(userData) {
const res = await login(form).catch(_ => false) // useAppStore().getAppConfig()
if (res) {
useAppStore().getAppConfig()
const userData = res.data
setToken(userData.token) setToken(userData.token)
// //
localStorage.setItem('user_info', JSON.stringify({ name: userData.username })) localStorage.setItem('user_info', JSON.stringify({ name: userData.username }))
@@ -38,12 +37,20 @@ export const useUserStore = defineStore({
if (userData.route_names && userData.route_names.length) { if (userData.route_names && userData.route_names.length) {
useRouteStore().addRoutes(userData.route_names) useRouteStore().addRoutes(userData.route_names)
} }
},
async login(form) {
const res = await login(form).catch(_ => false)
if (res) {
useAppStore().getAppConfig()
const userData = res.data
this.saveUserData(userData)
return userData return userData
} else { } else {
return false return false
} }
}, },
async info () { async info() {
const res = await current().catch(_ => false) const res = await current().catch(_ => false)
if (res) { if (res) {
useAppStore().getAppConfig() useAppStore().getAppConfig()
@@ -57,6 +64,41 @@ export const useUserStore = defineStore({
} }
return false return false
}, },
async oidc(provider, platform, browser) {
// oidc data need to be implement
const data = {
deviceInfo: {
name: navigator.userAgent, // 使用浏览器的 User-Agent 作为设备名
os: platform, // 获取操作系统信息
type: 'webadmin', // any vaule
},
id: `${platform}-${browser}`,
op: provider, // 传入的 provider
uuid: crypto.randomUUID(), // 自动生成 UUID
};
const res = await oidcAuth(data).catch(_ => false)
if (res) {
const { code, url } = res.data
setCode(code)
if (provider == 'webauth') {
window.open(url)
} else {
window.location.href = url
}
}
},
async query(code) {
const params = { "code": code, "uuid": crypto.randomUUID(), "Id": "999" }
const res = await oidcQuery(params).catch(_ => false)
if (res) {
removeCode()
useAppStore().getAppConfig()
const userData = res.data
this.saveUserData(userData)
return userData
}
return false
}
}, },
}) })
+30
View File
@@ -1,4 +1,6 @@
const TokenKey = 'access_token' const TokenKey = 'access_token'
const OidcCode = 'oidc_code'
const OidcCodeExpiry = 'oidc_code_expiry';
export function getToken () { export function getToken () {
return localStorage.getItem(TokenKey) return localStorage.getItem(TokenKey)
@@ -11,3 +13,31 @@ export function setToken (token) {
export function removeToken () { export function removeToken () {
return localStorage.removeItem(TokenKey) return localStorage.removeItem(TokenKey)
} }
// 设置 code,并存储当前时间戳(单位:毫秒)
export function setCode(code) {
const now = Date.now(); // 当前时间戳(毫秒)
const expiry = now + 30 * 1000; // 30 秒后过期
localStorage.setItem(OidcCode, code); // 存储 code
localStorage.setItem(OidcCodeExpiry, expiry); // 存储过期时间戳
}
// 获取 code,如果已过期则删除并返回 null
export function getCode() {
const expiry = localStorage.getItem(OidcCodeExpiry); // 获取过期时间戳
const now = Date.now(); // 当前时间戳
if (expiry && now > parseInt(expiry)) {
// 如果已过期,删除 code 和过期时间
removeCode();
return null;
}
return localStorage.getItem(OidcCode); // 返回 code(如果未过期)
}
// 删除 code 和过期时间
export function removeCode() {
localStorage.removeItem(OidcCode);
localStorage.removeItem(OidcCodeExpiry);
}
+4
View File
@@ -427,5 +427,9 @@
}, },
"LastOnlineIp": { "LastOnlineIp": {
"One": "最后在线IP" "One": "最后在线IP"
},
"or login in with" :
{
"One": "或使用以下登陆"
} }
} }
+6
View File
@@ -55,6 +55,12 @@ service.interceptors.response.use(
response => { response => {
const res = response.data const res = response.data
// for the endpoint /login-options
// I'm not sure if this is a good idea
if (Array.isArray(res)) {
return res;
}
// if the custom code is not 20000, it is judged as an error. // if the custom code is not 20000, it is judged as an error.
if (res.code !== 0) { if (res.code !== 0) {
ElMessage({ ElMessage({
+200 -46
View File
@@ -1,88 +1,244 @@
<template> <template>
<div class="login"> <div class="login-container">
<el-card class="login-card"> <div class="login-card">
<h1>{{ T('Login') }}</h1> <img src="@/assets/logo.png" alt="logo" class="login-logo" />
<el-form label-width="100px">
<el-form-item :label=" T('Username') "> <el-form label-position="top" class="login-form">
<el-input v-model="form.username"></el-input> <el-form-item :label="T('Username')">
<el-input v-model="form.username" class="login-input"></el-input>
</el-form-item> </el-form-item>
<el-form-item :label=" T('Password') ">
<el-input v-model="form.password" type="password" @keyup.enter.native="login" show-password></el-input> <el-form-item :label="T('Password')">
<el-input v-model="form.password" type="password" @keyup.enter.native="login" show-password
class="login-input"></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="login" type="primary">{{ T('Login') }}</el-button> <el-button @click="login" type="primary" class="login-button">{{ T('Login') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card>
<div class="divider" v-if="options.length > 0">
<span>{{ T('or login in with') }}</span>
</div>
<div class="oidc-options">
<div v-for="(option, index) in options" :key="index" class="oidc-option">
<el-button @click="handleOIDCLogin(option.name)" class="oidc-btn">
<img :src="getProviderImage(option.name)" alt="provider" class="oidc-icon" />
{{ T(option.name) }}
</el-button>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { defineComponent, reactive } from 'vue' import { reactive, onMounted, ref } from 'vue';
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus';
import { useRoute, useRouter } from 'vue-router' import { T } from '@/utils/i18n';
import { T } from '@/utils/i18n' import { useRoute, useRouter } from 'vue-router';
import { loginOptions, oidcAuth, oidcQuery } from '@/api/login';
import { getCode, removeCode } from '@/utils/auth'
const userStore = useUserStore() const oauthInfo = ref({})
const route = useRoute() const userStore = useUserStore()
const router = useRouter() const route = useRoute()
const router = useRouter()
const options = reactive([]); // 存储 OIDC 登录选项
let platform = window.navigator.platform let platform = window.navigator.platform
if (navigator.platform.indexOf('Mac') === 0) { if (navigator.platform.indexOf('Mac') === 0) {
platform = 'mac' platform = 'mac'
} else if (navigator.platform.indexOf('Win') === 0) { } else if (navigator.platform.indexOf('Win') === 0) {
platform = 'windows' platform = 'windows'
} else if (navigator.platform.indexOf('Linux armv') === 0) { } else if (navigator.platform.indexOf('Linux armv') === 0) {
platform = 'android' platform = 'android'
} else if (navigator.platform.indexOf('Linux') === 0) { } else if (navigator.platform.indexOf('Linux') === 0) {
platform = 'linux' platform = 'linux'
} }
const userAgent = navigator.userAgent;
let browser = 'Unknown Browser';
if (/chrome|crios/i.test(userAgent)) browser = 'Chrome';
else if (/firefox|fxios/i.test(userAgent)) browser = 'Firefox';
else if (/safari/i.test(userAgent) && !/chrome/i.test(userAgent)) browser = 'Safari';
else if (/edg/i.test(userAgent)) browser = 'Edge';
const form = reactive({ const form = reactive({
username: '', username: '',
password: '', password: '',
platform: platform, platform: platform,
}) })
const redirect = route.query?.redirect
const login = async () => { const redirect = route.query?.redirect
const login = async () => {
const res = await userStore.login(form) const res = await userStore.login(form)
if (res) { if (res) {
ElMessage.success(T('LoginSuccess')) ElMessage.success(T('LoginSuccess'))
router.push({ path: redirect || '/', replace: true }) router.push({ path: redirect || '/', replace: true })
} }
}
const handleOIDCLogin = (provider) => {
userStore.oidc(provider, platform, browser)
};
const providerImageMap = {
google: '/google.png',
github: '/github.png',
oidc: '/oidc.png',
webauth: '/webauth.png',
default: '/default.png',
};
const getProviderImage = (provider) => {
return providerImageMap[provider] || providerImageMap.default;
};
const loadLoginOptions = async () => {
try {
const res = await loginOptions().catch(() => []);
if (!Array.isArray(res) || !res.length) return console.warn('No valid response received');
const jsonPart = res[0].split('/')[1];
if (!jsonPart) return console.error('Invalid input string:', res[0]);
// const ops = JSON.parse(jsonPart).map(option => ({ name: option.name }));
// 不确定怎么处理webauth,不显示
// 解析 JSON,并过滤掉 "webauth" 类型的选项
const ops = JSON.parse(jsonPart)
.filter(option => option.name !== "webauth") // 排除 "webauth" 类型的选项
.map(option => ({ name: option.name })); // 创建新的对象数组
if (!ops.length) return;
options.push(...ops);
} catch (error) {
console.error('Error loading login options:', error.message);
} }
};
onMounted(async () => {
const code = getCode();
if (code) {
// 如果code存在,进行query获取user info
const res = await userStore.query(code)
if (res) {
// 删除code,确保跳转之前对code进行清楚
removeCode()
ElMessage.success(T('LoginSuccess'))
router.push({ path: redirect || '/', replace: true })
}
} else {
// 如果code不存在, 现实登陆页面
loadLoginOptions(); // 组件挂载后调用登录选项加载函数
}
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.login { .login-container {
width: 100vw; display: flex;
justify-content: center;
align-items: center;
height: 100vh; height: 100vh;
background-color: #2d3a4b; background-color: #2d3a4b;
padding-top: 25vh; padding: 20px;
box-sizing: border-box; }
.tips { .login-card {
font-size: 12px; width: 360px;
color: #fff; background-color: #283342;
margin-left: 60px; padding: 40px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
h1 {
margin-bottom: 20px;
font-size: 24px;
font-weight: bold;
}
.login-form {
margin-bottom: 20px;
}
.login-input {
width: 100%;
}
.login-button {
width: 100%;
height: 40px;
margin-bottom: 20px;
}
.divider {
display: flex;
align-items: center;
margin: 20px 0;
font-size: 14px;
color: #888;
&::before,
&::after {
content: '';
flex: 1;
height: 1px;
background-color: #ddd;
} }
.login-card { &::before {
max-width: 500px; margin-right: 10px;
background-color: #283342; }
color: #fff;
border: none;
margin: 0 auto;
.el-form-item { &::after {
margin-left: 10px;
}
}
.oidc-options {
display: flex;
flex-direction: column;
gap: 10px;
}
.oidc-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
height: 50px;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
color: black;
font-size: 14px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.oidc-icon {
width: 24px;
height: 24px;
}
.login-logo {
width: 80px;
height: 80px;
margin: 0 auto 20px;
display: block;
}
.el-form-item {
::v-deep(.el-form-item__label) { ::v-deep(.el-form-item__label) {
color: #fff; color: #fff;
} }
.el-input { .el-input {
::v-deep(.el-input__wrapper) { ::v-deep(.el-input__wrapper) {
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
background: transparent; background: transparent;
@@ -92,7 +248,5 @@
color: #fff; color: #fff;
} }
} }
}
}
} }
</style> </style>