modify Login UI for OIDC login
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -427,5 +427,9 @@
|
|||||||
},
|
},
|
||||||
"LastOnlineIp": {
|
"LastOnlineIp": {
|
||||||
"One": "最后在线IP"
|
"One": "最后在线IP"
|
||||||
|
},
|
||||||
|
"or login in with" :
|
||||||
|
{
|
||||||
|
"One": "或使用以下登陆"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
Reference in New Issue
Block a user