Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/TickerQ.Dashboard/Endpoints/DashboardEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public static void MapDashboardEndpoints<TTimeTicker, TCronTicker>(this IEndpoin
.WithTags("TickerQ Dashboard")
.RequireCors("TickerQ_Dashboard_CORS")
.AllowAnonymous(), config);

WithGroupNameIfSet(endpoints.MapGet("/auth/challenge", (DashboardOptionsBuilder dashboardOptions) =>
dashboardOptions.Auth.Mode == AuthMode.Host ? Results.Challenge() : Results.Unauthorized())
.ExcludeFromDescription()
.AllowAnonymous(), config);

var apiGroup = endpoints.MapGroup("/api").WithTags("TickerQ Dashboard").RequireCors("TickerQ_Dashboard_CORS");
WithGroupNameIfSet(apiGroup, config);
Expand Down Expand Up @@ -247,6 +252,11 @@ private static async Task<IResult> ValidateAuth(HttpContext context, IAuthServic
}, dashboardOptions.DashboardJsonOptions);
}

if (dashboardOptions.Auth.Mode == AuthMode.Host)
{
return Results.Challenge();
}

return Results.Unauthorized();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ watch(isAuthenticated, (newValue) => {
<template>
<div class="auth-header">
<!-- Login Form -->
<div v-if="!isAuthenticated && showLoginForm" class="auth-section">
<div v-if="!isAuthenticated && showLoginForm && authMode !== 'none'" class="auth-section">
<div v-if="!shouldShowLoginForm" class="login-prompt">
<v-btn
color="primary"
Expand Down
16 changes: 16 additions & 0 deletions src/TickerQ.Dashboard/wwwroot/src/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ class AuthService {

clearTimeout(timeoutId);

// Handle transparent redirects from ASP.NET Core Challenge()
if (response.redirected && response.url) {
window.location.href = response.url;
// Return a non-resolving promise so execution halts while browser navigates
return new Promise<AuthStatus>(() => {});
}

if (response.ok) {
const result = await response.json();
return {
Expand All @@ -222,6 +229,15 @@ class AuthService {
};
}

// If the backend returned 401 directly without redirecting
if (response.status === 401 && this.config?.mode === 'host') {
// Use a non-API URL to trigger the browser's native navigation challenge flow
const config = window.TickerQConfig;
const baseUrl = config?.backendDomain || config?.basePath || '/tickerq/dashboard';
window.location.href = `${baseUrl}/auth/challenge`;
return new Promise<AuthStatus>(() => {});
}

return { authenticated: false, message: `Server error: ${response.status}` };
} catch (error) {
console.error('Credential validation failed:', error);
Expand Down
31 changes: 7 additions & 24 deletions src/TickerQ.Dashboard/wwwroot/src/views/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -131,36 +131,23 @@
@submit.prevent="handleLogin"
class="login-form mt-4"
>
<v-text-field
v-model="authStore.credentials.hostAccessKey"
label="Access Key"
placeholder="Bearer xyz123 or ApiKey abc456"
prepend-inner-icon="mdi-key-variant"
:rules="rules.hostAccessKey"
variant="outlined"
class="mb-6 login-input"
:disabled="authStore.isLoading"
@input="authStore.clearError()"
@keyup.enter="handleLogin"
autofocus
/>

<v-btn
type="submit"
color="primary"
size="large"
size="x-large"
block
:loading="authStore.isLoading"
:disabled="!isFormValid || authStore.isLoading"
:disabled="authStore.isLoading"
class="login-btn"
elevation="0"
>
<v-icon start>mdi-shield-key</v-icon>
{{ authStore.isLoading ? 'Setting Access Key...' : 'Set Access Key' }}
<v-icon start>mdi-login-variant</v-icon>
{{ authStore.isLoading ? 'Authenticating...' : 'Authenticate' }}
</v-btn>

<div class="auth-help-text">
<v-icon size="small" class="mr-1">mdi-information-outline</v-icon>
Enter your full access key (including Bearer/ApiKey prefix) for API calls
Click to validate if you are authenticated in the host application
</div>
</v-form>
</div>
Expand Down Expand Up @@ -207,10 +194,6 @@ const rules = {
apiKey: [
(v: string) => !!v || 'API key is required',
(v: string) => v.length >= 10 || 'API key must be at least 10 characters'
],
hostAccessKey: [
(v: string) => !!v || 'Access key is required',
(v: string) => v.length >= 10 || 'Access key must be at least 10 characters'
]
}

Expand All @@ -222,7 +205,7 @@ const isFormValid = computed(() => {
} else if (authMode.value === 'apikey') {
return (authStore.credentials.apiKey?.length || 0) >= 10
} else if (authMode.value === 'host') {
return (authStore.credentials.hostAccessKey?.length || 0) >= 10
return true
}
return false
})
Expand Down
Loading