fix: handle external login state and emit readiness event (#13493)

* fix: handle external login state and emit readiness event

* fix: handle external login state and improve SAML2 logout response handling
This commit is contained in:
蘭 authored and GitHub committed 2026-08-06 13:53:36 +08:00
1 parent c38d741770
commit 1971d9dec2
3 files changed
+71 -18

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
export interface ExternalLoginTickets {
oidcTicket: string;
saml2Ticket: string;
}
const oidcTicketParam = 'oidc_ticket';
const saml2TicketParam = 'saml2_ticket';
const getFragmentParams = (url: URL) => {
return new URLSearchParams(url.hash.startsWith('#') ? url.hash.slice(1) : url.hash);
};
export const parseExternalLoginTickets = (href: string): ExternalLoginTickets => {
const url = new URL(href);
const fragmentParams = getFragmentParams(url);
return {
oidcTicket: fragmentParams.get(oidcTicketParam) || url.searchParams.get(oidcTicketParam) || '',
saml2Ticket: fragmentParams.get(saml2TicketParam) || url.searchParams.get(saml2TicketParam) || '',
};
};
export const hasExternalLoginTicket = () => {
const { oidcTicket, saml2Ticket } = parseExternalLoginTickets(window.location.href);
return Boolean(oidcTicket || saml2Ticket);
};
export const takeExternalTicketsFromURL = (): ExternalLoginTickets => {
const tickets = parseExternalLoginTickets(window.location.href);
if (!tickets.oidcTicket && !tickets.saml2Ticket) return tickets;
const url = new URL(window.location.href);
const fragmentParams = getFragmentParams(url);
fragmentParams.delete(oidcTicketParam);
fragmentParams.delete(saml2TicketParam);
url.searchParams.delete(oidcTicketParam);
url.searchParams.delete(saml2TicketParam);
const sanitizedFragment = fragmentParams.toString();
url.hash = sanitizedFragment ? `#${sanitizedFragment}` : '';
const sanitizedURL = `${url.pathname}${url.search}${url.hash}`;
window.history.replaceState(window.history.state, '', sanitizedURL);
return tickets;
};
@@ -379,6 +379,7 @@ import { MenuStore, TabsStore } from '@/store';
import { MsgError, MsgSuccess } from '@/utils/message';
import { useI18n } from 'vue-i18n';
import { encryptPassword, base64UrlToBuffer, bufferToBase64Url } from '@/utils/auth';
import { takeExternalTicketsFromURL } from '@/utils/external-login';
import { getXpackSettingForTheme } from '@/utils/xpack';
import { routerToName } from '@/utils/router';
import { Key } from '@element-plus/icons-vue';
@@ -388,6 +389,9 @@ import { adjustColorToRGBA } from '@/utils/color';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { submitSAML2Navigation } from '@/utils/saml2';
const emit = defineEmits<{
(e: 'external-login-ready'): void;
}>();
const i18n = useI18n();
const {
globalStore,
@@ -429,22 +433,6 @@ const open = ref(false);
const loginBtnLinkColor = ref<string | null>(null);
let loginViewActive = true;
const takeExternalTicketsFromURL = () => {
const url = new URL(window.location.href);
const fragmentParams = new URLSearchParams(url.hash.startsWith('#') ? url.hash.slice(1) : url.hash);
const oidcTicket = fragmentParams.get('oidc_ticket') || url.searchParams.get('oidc_ticket') || '';
const saml2Ticket = fragmentParams.get('saml2_ticket') || url.searchParams.get('saml2_ticket') || '';
if (!oidcTicket && !saml2Ticket) return { oidcTicket: '', saml2Ticket: '' };
fragmentParams.delete('oidc_ticket');
fragmentParams.delete('saml2_ticket');
url.searchParams.delete('oidc_ticket');
url.searchParams.delete('saml2_ticket');
const sanitizedFragment = fragmentParams.toString();
url.hash = sanitizedFragment ? `#${sanitizedFragment}` : '';
const sanitizedURL = `${url.pathname}${url.search}${url.hash}`;
window.history.replaceState(window.history.state, '', sanitizedURL);
return { oidcTicket, saml2Ticket };
};
const pendingExternalTickets = takeExternalTicketsFromURL();
const pendingOIDCTicket = pendingExternalTickets.oidcTicket;
const pendingSAML2Ticket = pendingOIDCTicket ? '' : pendingExternalTickets.saml2Ticket;
@@ -596,6 +584,16 @@ const switchToLocalLogin = () => {
});
};
const navigateAfterLogin = async () => {
try {
await routerToName('home');
} catch {
if (hasPendingExternalTicket) {
emit('external-login-ready');
}
}
};
const completeLogin = async (result: LoginModel.ResLogin) => {
isLogin.value = true;
agreeLicense.value = true;
@@ -607,8 +605,8 @@ const completeLogin = async (result: LoginModel.ResLogin) => {
MsgSuccess(i18n.t('commons.msg.loginSuccess'));
localStorage.removeItem('dashboardCache');
localStorage.removeItem('upgradeChecked');
routerToName('home');
clearLoginKeydownHandler();
await navigateAfterLogin();
};
const handleLoginResult = async (result: LoginModel.ResLogin) => {
@@ -984,6 +982,9 @@ onMounted(async () => {
} else if (pendingSAML2Ticket) {
await finishSAML2Login(pendingSAML2Ticket);
}
if (hasPendingExternalTicket && !isLogin.value) {
emit('external-login-ready');
}
if (!loginViewActive || isLogin.value) return;
if (!isLogin.value && !mfaShow.value) {
await Promise.all([loadLDAPStatus(), loadOIDCStatus(), loadSAML2Status()]);
+11 -1
View File
@@ -2,6 +2,13 @@
<div class="flex items-center justify-center min-h-screen relative bg-gray-100">
<div class="absolute inset-0 bg-cover bg-center bg-no-repeat" :style="backgroundStyle"></div>
<div
v-if="externalLoginPending"
v-loading="true"
class="absolute inset-0 z-20 bg-gray-100"
aria-busy="true"
></div>
<div
v-show="!externalLoginPending"
:style="{ opacity: backgroundOpacity, width: containerWidth, height: containerHeight }"
class="bg-white shadow-lg relative z-10 border border-gray-200 flex overflow-hidden"
id="login-container"
@@ -18,7 +25,7 @@
/>
</div>
<div :class="loginFormClass">
<LoginForm ref="loginRef"></LoginForm>
<LoginForm ref="loginRef" @external-login-ready="externalLoginPending = false"></LoginForm>
</div>
</div>
</div>
@@ -30,6 +37,7 @@ import LoginForm from './components/login-form.vue';
import { ref, onMounted } from 'vue';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { preloadImage } from '@/utils/browser';
import { hasExternalLoginTicket } from '@/utils/external-login';
defineOptions({ name: 'Login' });
const { entrance, isEnterprise, themeConfig } = useGlobalStore();
const backgroundOpacity = ref(1);
@@ -42,6 +50,8 @@ const backgroundStyle = ref<{ backgroundImage?: string; backgroundColor?: string
const imgLoaded = ref(false);
const currentDefaultLoginImage = computed(() => (isEnterprise.value ? defaultEnterpriseLoginImage : defaultLoginImage));
const externalLoginPending = ref(hasExternalLoginTicket());
function onImgLoad() {
imgLoaded.value = true;
}