Skip to content
Open
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
1 change: 1 addition & 0 deletions console/apps/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@agent-management-platform/shared-component": "workspace:*",
"js-yaml": "4.1.1",
"zod": "4.3.6",
"@asgardeo/react": "0.19.0",
"@agent-management-platform/overview": "workspace:*",
"@agent-management-platform/build": "workspace:*",
"@agent-management-platform/deploy": "workspace:*",
Expand Down
37 changes: 19 additions & 18 deletions console/apps/webapp/public/config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
Expand All @@ -18,26 +18,27 @@

window.__RUNTIME_CONFIG__ = {
authConfig: {
signInRedirectURL: 'null',
signOutRedirectURL: 'null',
clientID: 'null',
baseUrl: 'null',
scope: ['openid', 'profile'],
storage: 'sessionStorage',
// Disable strict ID token validation for providers with non-standard issuers
// (e.g., Thunder uses "thunder" instead of a URL)
// Set VALIDATE_ID_TOKEN=true for providers that comply with OIDC standards (e.g., Asgardeo)
validateIDToken: '' === 'true',
// Clock tolerance (in seconds) to handle time skew between client and server
// Prevents token validation failures due to minor time differences
clockTolerance: 300
baseUrl: 'http://thunder.amp.localhost:8080',
clientId: 'amp-console-client',
signInUrl: 'http://thunder.amp.localhost:8080/gate',
afterSignInUrl: 'http://localhost:3001/login',
afterSignOutUrl: 'http://localhost:3001/login',
scopes: ['openid', 'profile', 'email'],
platform: 'AsgardeoV2',
tokenValidation: {
idToken: {
// Disable for Thunder / local dev with non-standard issuers or self-signed certs
validate: false,
clockTolerance: 300,
},
},
storage: 'localStorage',
},
disableAuth: 'true' === 'true',
disableAuth: true,
apiBaseUrl: 'http://localhost:9000',
gatewayControlPlaneUrl: 'http://localhost:9243',
gatewayVersion: '',
instrumentationUrl: '',
gatewayVersion: 'v0.9.0',
instrumentationUrl: 'http://localhost:22893/otel',
guardrailsCatalogUrl: 'https://db720294-98fd-40f4-85a1-cc6a3b65bc9a-prod.e1-us-east-azure.choreoapis.dev/api-platform/policy-hub-api/policy-hub-public/v1.0/policies?categories=Guardrails',
guardrailsDefinitionBaseUrl: 'https://db720294-98fd-40f4-85a1-cc6a3b65bc9a-prod.e1-us-east-azure.choreoapis.dev/api-platform/policy-hub-api/policy-hub-public/v1.0/policies',
};

25 changes: 13 additions & 12 deletions console/apps/webapp/public/config.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,20 @@

window.__RUNTIME_CONFIG__ = {
authConfig: {
signInRedirectURL: '$SIGN_IN_REDIRECT_URL',
signOutRedirectURL: '$SIGN_OUT_REDIRECT_URL',
clientID: '$AUTH_CLIENT_ID',
baseUrl: '$AUTH_BASE_URL',
scope: ['openid', 'profile'],
storage: 'sessionStorage',
// Disable strict ID token validation for providers with non-standard issuers
// (e.g., Thunder uses "thunder" instead of a URL)
// Set VALIDATE_ID_TOKEN=true for providers that comply with OIDC standards (e.g., Asgardeo)
validateIDToken: '$VALIDATE_ID_TOKEN' === 'true',
// Clock tolerance (in seconds) to handle time skew between client and server
// Prevents token validation failures due to minor time differences
clockTolerance: 300
clientId: '$AUTH_CLIENT_ID',
signInUrl: '$AUTH_BASE_URL/gate',
afterSignInUrl: '$SIGN_IN_REDIRECT_URL',
afterSignOutUrl: '$SIGN_OUT_REDIRECT_URL',
scopes: ('$AUTH_SCOPES'.trim() || 'openid profile email').split(/\s+/).filter(Boolean),
platform: 'AsgardeoV2',
tokenValidation: {
idToken: {
validate: '$VALIDATE_ID_TOKEN' === 'true',
clockTolerance: Number('$CLOCK_TOLERANCE') || 300,
},
},
storage: 'localStorage',
},
disableAuth: '$DISABLE_AUTH' === 'true',
apiBaseUrl: '$API_BASE_URL',
Expand Down
2 changes: 1 addition & 1 deletion console/apps/webapp/src/Layouts/userMenuItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ export const createUserMenuItems = ({ logout }: { logout: () => Promise<void> })
label: "Logout",
onClick:logout,
icon: <LogOut />,
href: globalConfig.authConfig.signOutRedirectURL,
href: globalConfig.authConfig.afterSignOutUrl ?? "/login",
},
];
2 changes: 1 addition & 1 deletion console/apps/webapp/src/pages/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function Login() {
return;
}

if (isAuthenticated || userInfo) {
if (isAuthenticated || userInfo?.sub) {
window.location.assign(safeRedirectPath);
return;
}
Expand Down
1,301 changes: 719 additions & 582 deletions console/common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions console/env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# Environment variables for Agent Management Console

# OAuth Configuration
CLIENT_ID=your-client-id
BASE_URL=https://your-auth-provider.com
SIGN_IN_REDIRECT_URL=https://your-domain.com
SIGN_OUT_REDIRECT_URL=https://your-domain.com
# OAuth / Thunder (Asgardeo V2) — used by envsubst on config.template.js
AUTH_CLIENT_ID=your-client-id
AUTH_BASE_URL=https://your-thunder-host
AUTH_SCOPES=openid profile email
SIGN_IN_REDIRECT_URL=https://your-domain.com/login
SIGN_OUT_REDIRECT_URL=https://your-domain.com/login
VALIDATE_ID_TOKEN=false
CLOCK_TOLERANCE=300

# API Configuration
API_BASE_URL=https://your-api.com
Expand Down
9 changes: 6 additions & 3 deletions console/workspaces/libs/api-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
"dev": "tsc -b --watch",
"lint:fix": "eslint . --fix"
},
"keywords": ["api", "client", "agent management platform"],
"keywords": [
"api",
"client",
"agent management platform"
],
"author": "",
"license": "ISC",
"devDependencies": {
Expand All @@ -27,7 +31,7 @@
"@babel/runtime-corejs3": "7.11.2"
},
"dependencies": {
"@asgardeo/auth-react": "3.0.0",
"@asgardeo/react": "0.19.0",
"react-router-dom": "6.28.0",
"react": "19.1.1",
"react-dom": "19.1.1",
Expand All @@ -42,4 +46,3 @@
"@agent-management-platform/views": "workspace:*"
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ function toTitleCase(value: string): string {
.join(" ");
}


function getQueryTarget(queryKey: QueryKey): string {
const root = Array.isArray(queryKey) ? queryKey[0] : queryKey;
return typeof root === "string" ? toTitleCase(root) : "data";
Expand All @@ -95,10 +94,35 @@ function getActionSuccessMessage(action: MutationActionConfig): string {
return `${toTitleCase(action.target)} ${SUCCESS_VERB_MAP[action.verb]} successfully`;
}

function shouldSuppressErrorSnackBar(error: unknown): boolean {
/**
* Handles auth/session-related failures (may call `logout`) and other cases
* where a generic error snackbar should not appear. Returns true when the
* error is considered handled for notification purposes.
*/
function handleAuthAndExpectedErrors(
error: unknown,
logout: () => void
): boolean {
if (
error &&
(error as { code?: string })?.code === "SPA-AUTH_CLIENT-VM-NF01"
) {
return true;
}
if (
error &&
(error as { code?: string })?.code === "SPA-AUTH_CLIENT-VM-IV02"
) {
logout();
return true;
}
const e = error as { status?: number; response?: { status?: number } };
const status = e.status ?? e.response?.status;
return status === 400 || status === 401;
if (status === 401) {
logout();
return true;
}
return status === 400;
}

export function useApiQuery<
Expand All @@ -110,7 +134,7 @@ export function useApiQuery<
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
): UseQueryResult<TData, TError> {
const { pushSnackBar } = useSnackBar();
const { isAuthenticated } = useAuthHooks();
const { isAuthenticated, logout } = useAuthHooks();
const query = useQuery(options);
const lastErrorMessageRef = useRef<string | null>(null);

Expand All @@ -120,6 +144,11 @@ export function useApiQuery<
return;
}

if (handleAuthAndExpectedErrors(query.error, logout)) {
lastErrorMessageRef.current = null;
return;
}

if (!isAuthenticated) {
lastErrorMessageRef.current = null;
return;
Expand Down Expand Up @@ -153,11 +182,6 @@ export function useApiQuery<
apiCallName = queryTarget;
}

if (shouldSuppressErrorSnackBar(query.error)) {
lastErrorMessageRef.current = null;
return;
}

const fallbackMessage = `Failed to fetch ${apiCallName}`;
// Always show only the generic message for any HTTP/network error
const errorMessage = fallbackMessage;
Expand All @@ -169,7 +193,14 @@ export function useApiQuery<

lastErrorMessageRef.current = errorMessage;
pushSnackBar({ message: errorMessage, type: "error" });
}, [isAuthenticated, options.queryKey, pushSnackBar, query.error, query.isError]);
}, [
isAuthenticated,
options.queryKey,
pushSnackBar,
query.error,
query.isError,
logout,
]);

return query;
}
Expand All @@ -183,7 +214,7 @@ export function useApiMutation<
options: ApiMutationOptions<TData, TError, TVariables, TContext>,
): UseMutationResult<TData, TError, TVariables, TContext> {
const { pushSnackBar } = useSnackBar();
const { isAuthenticated } = useAuthHooks();
const { isAuthenticated, logout } = useAuthHooks();
const {
action,
successMessage,
Expand All @@ -200,16 +231,22 @@ export function useApiMutation<
if (showSuccess && isAuthenticated) {
pushSnackBar({
message:
resolveMessage(successMessage, data, variables)
?? (action ? getActionSuccessMessage(action) : "Request completed successfully"),
resolveMessage(successMessage, data, variables) ??
(action
? getActionSuccessMessage(action)
: "Request completed successfully"),
type: "success",
});
}

onSuccess?.(data, variables, onMutateResult, context);
},
onError: (error, variables, onMutateResult, context) => {
if (showError && isAuthenticated && !shouldSuppressErrorSnackBar(error)) {
if (
showError &&
isAuthenticated &&
!handleAuthAndExpectedErrors(error, logout)
) {
// Determine subject for error message
const subject = action?.target || "data";
// Use a generic message for mutation errors
Expand Down
2 changes: 1 addition & 1 deletion console/workspaces/libs/api-client/src/hooks/traces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function useTraceList(
timeRange?: TraceListTimeRange | undefined,
limit?: number | undefined,
offset?: number | undefined,
sortOrder?: GetTraceListPathParams['sortOrder'] | undefined,
sortOrder?: GetTraceListPathParams["sortOrder"] | undefined,
customStartTime?: string,
customEndTime?: string,
) {
Expand Down
16 changes: 0 additions & 16 deletions console/workspaces/libs/api-client/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
* under the License.
*/

import { refreshToken } from '@agent-management-platform/auth';
import { globalConfig } from '@agent-management-platform/types';

export function sleep(ms: number): Promise<void> {
Expand All @@ -40,17 +39,6 @@ export interface HttpOptions {
useObsPlaneHostApi?: boolean;
}

/**
* Triggers a token refresh only when the response indicates an expired/invalid
* token (HTTP 401). Intentionally skips refresh for client errors such as 404
* (resource not found) or 400 (bad request) which are not auth-related.
*/
async function handleTokenExpiry(response: Response): Promise<void> {
if (response.status === 401) {
await refreshToken();
}
}

type HttpErrorWithStatus = Error & { status: number; body?: unknown };

async function throwIfHttpWriteNotOk(response: Response): Promise<void> {
Expand All @@ -76,9 +64,6 @@ async function throwIfHttpWriteNotOk(response: Response): Promise<void> {
}

async function finalizeHttpWriteResponse(response: Response): Promise<Response> {
if (!response.ok) {
await handleTokenExpiry(response);
}
await sleep(DEFAULT_TIMEOUT);
if (!response.ok) {
await throwIfHttpWriteNotOk(response);
Expand All @@ -101,7 +86,6 @@ export async function httpGET(
}
});
if (!response.ok) {
await handleTokenExpiry(response);
const err = new Error(`HTTP error! status: ${response.status}`) as HttpErrorWithStatus;
err.status = response.status;
throw err;
Expand Down
2 changes: 1 addition & 1 deletion console/workspaces/libs/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@babel/runtime-corejs3": "7.11.2"
},
"dependencies": {
"@asgardeo/auth-react": "3.0.0",
"@asgardeo/react": "0.19.0",
"react-router-dom": "6.28.0",
"react": "19.1.1",
"react-dom": "19.1.1",
Expand Down
32 changes: 7 additions & 25 deletions console/workspaces/libs/auth/src/asgardio/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,19 @@
* under the License.
*/

import { AuthProvider as AsgardeoAuthProvider, useAuthContext } from '@asgardeo/auth-react';
import { useEffect } from 'react';
import { globalConfig } from '@agent-management-platform/types';
import { AuthProviderProps } from '../types';
import { initRefreshToken } from './hooks/authHooks';
import {
AsgardeoProvider,
} from "@asgardeo/react";
import { globalConfig } from "@agent-management-platform/types";
import { AuthProviderProps } from "../types";

/**
* Runs inside AsgardeoAuthProvider so it can access the provider-managed
* auth context. Stores `refreshAccessToken` in the module-level ref so the
* plain `refreshToken` utility (used outside React) calls the same session.
*/
const TokenRefreshSetup: React.FC = () => {
const { refreshAccessToken } = useAuthContext() ?? {};

useEffect(() => {
if (refreshAccessToken) {
initRefreshToken(refreshAccessToken);
}
}, [refreshAccessToken]);

return null;
};

export const AuthProvider = ({ children }: AuthProviderProps) => {
const { authConfig } = globalConfig;

return (
<AsgardeoAuthProvider config={authConfig}>
<TokenRefreshSetup />
<AsgardeoProvider {...authConfig}>
{children}
</AsgardeoAuthProvider>
</AsgardeoProvider>
);
};

Loading
Loading