mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-10 21:21:13 +08:00
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:
3 files changed
+71
-18
No files matched your search
@@ -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()]);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user