@@ -80,6 +80,11 @@ const logo = ref(logoSource)
8080const headerLinksHoverColor = ref (headerLinksHoverColorSetting )
8181const headerLinksBackgroundColor = ref (headerLinksBackgroundColorSetting )
8282
83+ // Multi-provider support
84+ const availableProviders = ref <Array <{ name: string ; available: boolean ; lastChecked? : Date ; error? : string }>>([])
85+ const showProviderSelector = ref (false )
86+ const isLoadingProviders = ref (false )
87+
8388// Check OAuth2 availability
8489let oauth2CheckInterval: number | null = null
8590
@@ -104,6 +109,74 @@ async function checkOAuth2Availability() {
104109 }
105110}
106111
112+ // Fetch available OIDC providers
113+ async function fetchAvailableProviders() {
114+ isLoadingProviders .value = true
115+ try {
116+ const response = await fetch (' /api/oauth2/providers' )
117+ const data = await response .json ()
118+
119+ if (data .providers && Array .isArray (data .providers )) {
120+ availableProviders .value = data .providers
121+ console .log (' Available OAuth2 providers:' , availableProviders .value )
122+ console .log (` Total: ${data .count }, Available: ${data .availableCount } ` )
123+ } else {
124+ console .warn (' No providers returned from /api/oauth2/providers' )
125+ availableProviders .value = []
126+ }
127+ } catch (error ) {
128+ console .error (' Failed to fetch OAuth2 providers:' , error )
129+ availableProviders .value = []
130+ } finally {
131+ isLoadingProviders .value = false
132+ }
133+ }
134+
135+ // Handle login button click
136+ function handleLoginClick() {
137+ const available = availableProviders .value .filter (p => p .available )
138+
139+ if (available .length > 1 ) {
140+ // Show provider selection dialog
141+ showProviderSelector .value = true
142+ } else if (available .length === 1 ) {
143+ // Direct login with single provider
144+ loginWithProvider (available [0 ].name )
145+ } else {
146+ // Fallback to legacy login (no provider parameter)
147+ window .location .href = ' /api/oauth2/connect?redirect=' + encodeURIComponent (getCurrentPath ())
148+ }
149+ }
150+
151+ // Login with selected provider
152+ function loginWithProvider(provider : string ) {
153+ const redirectUrl = ' /api/oauth2/connect?provider=' +
154+ encodeURIComponent (provider ) +
155+ ' &redirect=' +
156+ encodeURIComponent (getCurrentPath ())
157+ console .log (` Logging in with provider: ${provider } ` )
158+ window .location .href = redirectUrl
159+ }
160+
161+ // Format provider name for display
162+ function formatProviderName(name : string ): string {
163+ // Convert "obp-oidc" to "OBP OIDC", "keycloak" to "Keycloak", etc.
164+ return name .split (' -' )
165+ .map (word => word .charAt (0 ).toUpperCase () + word .slice (1 ))
166+ .join (' ' )
167+ }
168+
169+ // Get provider icon
170+ function getProviderIcon(name : string ): string {
171+ const icons: Record <string , string > = {
172+ ' obp-oidc' : ' 🏦' ,
173+ ' keycloak' : ' 🔐' ,
174+ ' google' : ' 🔵' ,
175+ ' github' : ' 🐙'
176+ }
177+ return icons [name ] || ' 🔑'
178+ }
179+
107180const clearActiveTab = () => {
108181 const activeLinks = document .querySelectorAll <HTMLElement >(' .router-link' )
109182 for (const active of activeLinks ) {
@@ -154,6 +227,9 @@ onMounted(async () => {
154227 // Initial OAuth2 availability check
155228 await checkOAuth2Availability ()
156229
230+ // Fetch available providers
231+ await fetchAvailableProviders ()
232+
157233 // Start continuous polling every 4 minutes to detect OIDC outages
158234 console .log (' OAuth2: Starting continuous monitoring (every 4 minutes)...' )
159235 oauth2CheckInterval = window .setInterval (checkOAuth2Availability , 240000 ) // 4 minutes
@@ -249,15 +325,50 @@ const getCurrentPath = () => {
249325 {{ $t('header.login') }}
250326 </button >
251327 </el-tooltip >
252- <a v-else-if =" isShowLoginButton && oauth2Available" v-bind:href =" '/api/oauth2/connect?redirect='+ encodeURIComponent(getCurrentPath())" class =" login-button router-link" id =" login" >
328+ <button
329+ v-else-if =" isShowLoginButton && oauth2Available"
330+ @click =" handleLoginClick"
331+ class =" login-button router-link"
332+ id =" login"
333+ >
253334 {{ $t('header.login') }}
254- </a >
335+ <span v-if =" availableProviders.filter(p => p.available).length > 1" style =" margin-left : 4px ;" >▼</span >
336+ </button >
255337 <span v-show =" isShowLogOffButton" class =" login-user" >{{ loginUsername }}</span >
256338 <a v-bind:href =" '/api/user/logoff?redirect=' + encodeURIComponent(getCurrentPath())" v-show =" isShowLogOffButton" class =" logoff-button router-link" id =" logoff" >
257339 {{ $t('header.logoff') }}
258340 </a >
259341 </RouterView >
260342 </nav >
343+
344+ <!-- Provider Selection Dialog -->
345+ <el-dialog
346+ v-model =" showProviderSelector"
347+ title =" Select Identity Provider"
348+ width =" 450px"
349+ :close-on-click-modal =" true"
350+ >
351+ <div class =" provider-list" >
352+ <div
353+ v-for =" provider in availableProviders.filter(p => p.available)"
354+ :key =" provider.name"
355+ class =" provider-item"
356+ @click =" loginWithProvider(provider.name); showProviderSelector = false"
357+ >
358+ <div class =" provider-icon" >{{ getProviderIcon(provider.name) }}</div >
359+ <div class =" provider-info" >
360+ <h4 >{{ formatProviderName(provider.name) }}</h4 >
361+ <span class =" provider-status" >Available</span >
362+ </div >
363+ <div class =" provider-arrow" >→</div >
364+ </div >
365+
366+ <div v-if =" availableProviders.filter(p => p.available).length === 0" class =" no-providers" >
367+ <p >No identity providers available</p >
368+ <p class =" error-hint" >Please contact your administrator</p >
369+ </div >
370+ </div >
371+ </el-dialog >
261372</template >
262373
263374<style >
@@ -320,11 +431,13 @@ nav {
320431}
321432
322433a .login-button ,
323- a .logoff-button {
434+ a .logoff-button ,
435+ button .login-button {
324436 margin : 5px ;
325437 color : #ffffff ;
326438 background-color : #32b9ce ;
327439 cursor : pointer ;
440+ border : none ;
328441}
329442
330443button .login-button-disabled {
@@ -351,4 +464,73 @@ button.login-button-disabled {
351464 display : inline-block ;
352465 vertical-align : middle ;
353466}
467+
468+ /* Provider Selection Dialog */
469+ .provider-list {
470+ display : flex ;
471+ flex-direction : column ;
472+ gap : 12px ;
473+ }
474+
475+ .provider-item {
476+ display : flex ;
477+ align-items : center ;
478+ padding : 16px ;
479+ border : 2px solid #e0e0e0 ;
480+ border-radius : 8px ;
481+ cursor : pointer ;
482+ transition : all 0.2s ;
483+ background-color : #ffffff ;
484+ }
485+
486+ .provider-item :hover {
487+ border-color : #32b9ce ;
488+ background-color : #f0f9fa ;
489+ transform : translateX (4px );
490+ }
491+
492+ .provider-icon {
493+ font-size : 32px ;
494+ margin-right : 16px ;
495+ min-width : 40px ;
496+ text-align : center ;
497+ }
498+
499+ .provider-info {
500+ flex : 1 ;
501+ }
502+
503+ .provider-info h4 {
504+ margin : 0 0 4px 0 ;
505+ font-size : 16px ;
506+ color : #39455f ;
507+ font-weight : 500 ;
508+ }
509+
510+ .provider-status {
511+ font-size : 12px ;
512+ color : #10b981 ;
513+ font-weight : 500 ;
514+ }
515+
516+ .provider-arrow {
517+ font-size : 20px ;
518+ color : #32b9ce ;
519+ margin-left : 12px ;
520+ }
521+
522+ .no-providers {
523+ text-align : center ;
524+ padding : 32px ;
525+ color : #999 ;
526+ }
527+
528+ .no-providers p {
529+ margin : 8px 0 ;
530+ }
531+
532+ .error-hint {
533+ font-size : 12px ;
534+ color : #999 ;
535+ }
354536 </style >
0 commit comments