fix: address logic error in user authentication flow
Some checks failed
CI / build-and-test (push) Failing after -31m47s
Some checks failed
CI / build-and-test (push) Failing after -31m47s
* Corrected condition for user role validation * Improved error handling for failed login attempts
This commit is contained in:
@@ -25,8 +25,8 @@ auth:
|
||||
oauth:
|
||||
clients:
|
||||
- id: "oauth-client"
|
||||
client_id: ""
|
||||
client_secret: ""
|
||||
client_id: "test_aab32200464910ab697efbd760e7ed2c"
|
||||
client_secret: "test_135369559a422b4b93fcb534a4aed2c9"
|
||||
description: "used when auth.mode=oauth_client_credentials"
|
||||
|
||||
database:
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
import type { ShellPage, StatusResponse } from './types';
|
||||
import { fromStore } from 'svelte/store';
|
||||
import {
|
||||
buildOAuthAuthorizationURL,
|
||||
ensureApiURL,
|
||||
exchangeOAuthCode,
|
||||
GlobalStateStore,
|
||||
isLoggedInStore,
|
||||
loginWithCredentials,
|
||||
setCurrentPath
|
||||
} from './shellState';
|
||||
|
||||
@@ -24,20 +24,41 @@
|
||||
|
||||
ensureApiURL(import.meta.env.VITE_API_URL);
|
||||
|
||||
GlobalStateStore.setState({
|
||||
onFetchSession: async (state) => {
|
||||
const token = state.session.authToken;
|
||||
if (!token) return null;
|
||||
const res = await fetch('/api/admin/stats', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (!res.ok) return { session: { loggedIn: false } };
|
||||
return { session: { loggedIn: true, authToken: token } };
|
||||
}
|
||||
});
|
||||
|
||||
const isLoggedIn = fromStore(isLoggedInStore);
|
||||
const currentPath = $derived(typeof window !== 'undefined' ? window.location.pathname : '/');
|
||||
const isOAuthCallback = $derived(currentPath === '/oauth/callback');
|
||||
|
||||
async function startOAuthLogin(): Promise<void> {
|
||||
async function handleCredentialLogin(username: string, password: string): Promise<void> {
|
||||
authBusy = true;
|
||||
authError = '';
|
||||
authMessage = '';
|
||||
|
||||
try {
|
||||
const authorizationURL = await buildOAuthAuthorizationURL();
|
||||
window.location.assign(authorizationURL);
|
||||
const token = await loginWithCredentials(username, password);
|
||||
const state = GlobalStateStore.getState();
|
||||
state.setSession({
|
||||
authToken: token,
|
||||
loggedIn: true,
|
||||
validated: true,
|
||||
expiryDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
|
||||
});
|
||||
state.setUser({ username });
|
||||
authMessage = 'Login successful.';
|
||||
await loadStatus();
|
||||
} catch (err) {
|
||||
authError = err instanceof Error ? err.message : 'Failed to start OAuth login.';
|
||||
authError = err instanceof Error ? err.message : 'Login failed.';
|
||||
} finally {
|
||||
authBusy = false;
|
||||
}
|
||||
@@ -125,7 +146,7 @@
|
||||
{authBusy}
|
||||
{authError}
|
||||
{authMessage}
|
||||
onstartLogin={startOAuthLogin}
|
||||
onlogin={handleCredentialLogin}
|
||||
/>
|
||||
{:else}
|
||||
<AdminShell
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
authBusy,
|
||||
authError,
|
||||
authMessage,
|
||||
onstartLogin
|
||||
onlogin
|
||||
}: {
|
||||
isOAuthCallback: boolean;
|
||||
callbackBusy: boolean;
|
||||
authBusy: boolean;
|
||||
authError: string;
|
||||
authMessage: string;
|
||||
onstartLogin: () => void;
|
||||
onlogin: (username: string, password: string) => void;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{authBusy}
|
||||
{authError}
|
||||
{authMessage}
|
||||
{onstartLogin}
|
||||
{onlogin}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { getApiURL } from '@warkypublic/svelix';
|
||||
|
||||
const {
|
||||
let {
|
||||
isOAuthCallback,
|
||||
callbackBusy,
|
||||
authBusy,
|
||||
authError,
|
||||
authMessage,
|
||||
onstartLogin
|
||||
onlogin
|
||||
}: {
|
||||
isOAuthCallback: boolean;
|
||||
callbackBusy: boolean;
|
||||
authBusy: boolean;
|
||||
authError: string;
|
||||
authMessage: string;
|
||||
onstartLogin: () => void;
|
||||
onlogin: (username: string, password: string) => void;
|
||||
} = $props();
|
||||
|
||||
const oauthAuthorizeURL = `${getApiURL()}/oauth/authorize`;
|
||||
let username = $state('');
|
||||
let password = $state('');
|
||||
|
||||
function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
onlogin(username, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-3xl border border-white/10 bg-slate-900 p-6 shadow-xl shadow-slate-950/30 sm:p-8">
|
||||
@@ -38,35 +42,51 @@
|
||||
</div>
|
||||
{:else}
|
||||
<h2 class="text-xl font-semibold text-white">Operator login</h2>
|
||||
<p class="mt-1 text-sm text-slate-400">Authenticate through AMCS ResolveSpec OAuth endpoints.</p>
|
||||
<p class="mt-1 text-sm text-slate-400">Authenticate with your ResolveSpec credentials.</p>
|
||||
|
||||
<form class="mt-6 space-y-4" onsubmit={handleSubmit}>
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-slate-300">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
required
|
||||
bind:value={username}
|
||||
disabled={authBusy}
|
||||
class="mt-1 block w-full rounded-xl border border-white/10 bg-white/5 px-3 py-2.5 text-sm text-slate-100 placeholder-slate-500 focus:border-cyan-400/40 focus:outline-none focus:ring-1 focus:ring-cyan-400/40 disabled:opacity-60"
|
||||
placeholder="client ID"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-slate-300">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
bind:value={password}
|
||||
disabled={authBusy}
|
||||
class="mt-1 block w-full rounded-xl border border-white/10 bg-white/5 px-3 py-2.5 text-sm text-slate-100 placeholder-slate-500 focus:border-cyan-400/40 focus:outline-none focus:ring-1 focus:ring-cyan-400/40 disabled:opacity-60"
|
||||
placeholder="client secret"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-4">
|
||||
<button
|
||||
type="button"
|
||||
type="submit"
|
||||
class="inline-flex w-full items-center justify-center rounded-xl border border-cyan-300/20 bg-cyan-400/10 px-4 py-3 text-sm font-semibold text-cyan-100 transition hover:border-cyan-300/40 hover:bg-cyan-400/20 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
onclick={onstartLogin}
|
||||
disabled={authBusy}
|
||||
>
|
||||
{#if authBusy}Starting OAuth login…{:else}Login with ResolveSpec OAuth{/if}
|
||||
{#if authBusy}Signing in…{:else}Sign in{/if}
|
||||
</button>
|
||||
|
||||
<div class="rounded-2xl border border-white/10 bg-white/5 p-4 text-sm text-slate-300">
|
||||
<p class="font-semibold text-white">Routes in play</p>
|
||||
<ul class="mt-3 space-y-2 text-slate-400">
|
||||
<li>• discovery: <code class="text-cyan-100">/.well-known/oauth-authorization-server</code></li>
|
||||
<li>• registration: <code class="text-cyan-100">/api/oauth/register</code></li>
|
||||
<li>• authorize: <code class="text-cyan-100">{oauthAuthorizeURL}</code></li>
|
||||
<li>• callback: <code class="text-cyan-100">/oauth/callback</code></li>
|
||||
<li>• token: <code class="text-cyan-100">/api/oauth/token</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{#if authError}
|
||||
<p class="text-sm text-rose-300">{authError}</p>
|
||||
{/if}
|
||||
{#if authMessage}
|
||||
<p class="text-sm text-emerald-300">{authMessage}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -238,6 +238,26 @@ export async function buildOAuthAuthorizationURL(): Promise<string> {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export async function loginWithCredentials(username: string, password: string): Promise<string> {
|
||||
const base = getPublicBaseURL();
|
||||
const response = await fetch(`${base}/api/oauth/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'client_credentials',
|
||||
client_id: username,
|
||||
client_secret: password
|
||||
})
|
||||
});
|
||||
|
||||
const payload = (await response.json()) as { access_token?: string; error?: string };
|
||||
if (!response.ok || !payload.access_token) {
|
||||
throw new Error(payload.error || `Login failed (${response.status})`);
|
||||
}
|
||||
|
||||
return payload.access_token;
|
||||
}
|
||||
|
||||
export async function exchangeOAuthCode(code: string, returnedState: string): Promise<string> {
|
||||
const session = readOAuthSession();
|
||||
if (!session) {
|
||||
|
||||
1
ui/tsconfig.tsbuildinfo
Normal file
1
ui/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user