Skip to content

Commit 3a03812

Browse files
committed
Add multi-provider login UI to HeaderNav
- Fetch available providers from /api/oauth2/providers on mount - Show provider selection dialog when multiple providers available - Direct login when only one provider available - Fallback to legacy mode when no providers configured - Display provider icons and formatted names - Responsive provider selection dialog with hover effects - Maintain backward compatibility with single-provider mode
1 parent 07d47ca commit 3a03812

1 file changed

Lines changed: 185 additions & 3 deletions

File tree

src/components/HeaderNav.vue

Lines changed: 185 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ const logo = ref(logoSource)
8080
const headerLinksHoverColor = ref(headerLinksHoverColorSetting)
8181
const 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
8489
let 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+
107180
const 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
322433
a.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
330443
button.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

Comments
 (0)