mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
feat: native Hanzo IAM integration for lux.id OIDC login
- Fix token exchange endpoint to /api/login/oauth/access_token (IAM convention) - Fix authorize URL to /login/oauth/authorize (consistent across components) - Use lux-app-client-id (registered in IAM) instead of unregistered 'explore' - Fetch user profile directly from IAM /api/userinfo endpoint - Skip Blockscout auth_logout API call for OIDC provider - Hide "scanning new transactions" warning (looks like error on low-traffic chains) - Fix type error in useRedirectForInvalidAuthToken for OIDC error shape
This commit is contained in:
+1
-1
@@ -37,7 +37,7 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=["daily_txs", "coin_price", "market_cap"]
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
|
||||
NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=oidc
|
||||
NEXT_PUBLIC_OIDC_SERVER_URL=https://lux.id
|
||||
NEXT_PUBLIC_OIDC_CLIENT_ID=explore
|
||||
NEXT_PUBLIC_OIDC_CLIENT_ID=lux-app-client-id
|
||||
|
||||
# Stats
|
||||
NEXT_PUBLIC_STATS_API_HOST=https://api-explore.lux.network/api/stats
|
||||
|
||||
@@ -51,7 +51,7 @@ const OidcCallback: NextPage = () => {
|
||||
const { serverUrl, clientId } = oidc;
|
||||
const redirectUri = `${ window.location.origin }/auth/callback`;
|
||||
|
||||
const tokenResponse = await fetch(`${ serverUrl }/oauth/token`, {
|
||||
const tokenResponse = await fetch(`${ serverUrl }/api/login/oauth/access_token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
|
||||
@@ -61,7 +61,7 @@ const SocketNewItemsNotice = chakra(({ children, className, url, num, showErrorA
|
||||
}
|
||||
|
||||
if (!num) {
|
||||
return `scanning new ${ name }s...`;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type === 'cross_chain_transaction') {
|
||||
@@ -78,10 +78,14 @@ const SocketNewItemsNotice = chakra(({ children, className, url, num, showErrorA
|
||||
);
|
||||
})();
|
||||
|
||||
if (!alertContent && !isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = !isLoading ? (
|
||||
<Alert
|
||||
className={ className }
|
||||
status={ showErrorAlert || !num ? 'warning_table' : 'info' }
|
||||
status={ showErrorAlert ? 'warning_table' : 'info' }
|
||||
px={ 4 }
|
||||
py="6px"
|
||||
textStyle="sm"
|
||||
|
||||
@@ -36,7 +36,7 @@ function getOidcLoginUrl(): string {
|
||||
state,
|
||||
});
|
||||
|
||||
return `${ serverUrl }/oauth/authorize?${ params.toString() }`;
|
||||
return `${ serverUrl }/login/oauth/authorize?${ params.toString() }`;
|
||||
}
|
||||
|
||||
const AuthGuardOidc = ({ children, onAuthSuccess, ensureEmail }: Props) => {
|
||||
|
||||
@@ -30,7 +30,11 @@ export default function useLogout() {
|
||||
|
||||
return React.useCallback(async() => {
|
||||
try {
|
||||
await apiFetch('general:auth_logout');
|
||||
const accountFeature = config.features.account;
|
||||
const isOidc = accountFeature.isEnabled && accountFeature.authProvider === 'oidc';
|
||||
if (!isOidc) {
|
||||
await apiFetch('general:auth_logout');
|
||||
}
|
||||
cookies.remove(cookies.NAMES.API_TOKEN);
|
||||
|
||||
if (config.features.rewards.isEnabled) {
|
||||
@@ -51,6 +55,9 @@ export default function useLogout() {
|
||||
queryKey: getResourceKey('general:user_info'),
|
||||
exact: true,
|
||||
});
|
||||
queryClient.resetQueries({
|
||||
queryKey: [ 'oidc_profile' ],
|
||||
});
|
||||
queryClient.resetQueries({
|
||||
queryKey: getResourceKey('general:custom_abi'),
|
||||
exact: true,
|
||||
|
||||
@@ -1,12 +1,60 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { UserInfo } from 'types/api/account';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import * as cookies from 'lib/cookies';
|
||||
|
||||
export default function useProfileQuery() {
|
||||
return useApiQuery('general:user_info', {
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
enabled: config.features.account.isEnabled && Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
|
||||
interface OidcUserInfo {
|
||||
readonly sub?: string;
|
||||
readonly name?: string;
|
||||
readonly preferred_username?: string;
|
||||
readonly displayName?: string;
|
||||
readonly email?: string;
|
||||
readonly picture?: string;
|
||||
readonly avatar?: string;
|
||||
}
|
||||
|
||||
function useOidcProfileQuery() {
|
||||
const feature = config.features.account;
|
||||
const serverUrl = feature.isEnabled && feature.authProvider === 'oidc' && feature.oidc ? feature.oidc.serverUrl : '';
|
||||
const token = cookies.get(cookies.NAMES.API_TOKEN);
|
||||
|
||||
return useQuery<UserInfo>({
|
||||
queryKey: [ 'oidc_profile', serverUrl ],
|
||||
queryFn: async() => {
|
||||
const response = await fetch(`${ serverUrl }/api/userinfo`, {
|
||||
headers: { Authorization: `Bearer ${ token }` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch profile');
|
||||
}
|
||||
const data = await response.json() as OidcUserInfo;
|
||||
return {
|
||||
name: data.name || data.displayName || undefined,
|
||||
nickname: data.preferred_username || undefined,
|
||||
email: data.email || null,
|
||||
address_hash: null,
|
||||
avatar: data.picture || data.avatar || undefined,
|
||||
};
|
||||
},
|
||||
refetchOnMount: false,
|
||||
enabled: Boolean(serverUrl && token),
|
||||
});
|
||||
}
|
||||
|
||||
export default function useProfileQuery() {
|
||||
const feature = config.features.account;
|
||||
const isOidc = feature.isEnabled && feature.authProvider === 'oidc';
|
||||
|
||||
const oidcQuery = useOidcProfileQuery();
|
||||
const blockscoutQuery = useApiQuery('general:user_info', {
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
enabled: !isOidc && feature.isEnabled && Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
|
||||
},
|
||||
});
|
||||
|
||||
return isOidc ? oidcQuery : blockscoutQuery;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
|
||||
export default function useRedirectForInvalidAuthToken() {
|
||||
const rollbar = useRollbar();
|
||||
const profileQuery = useProfileQuery();
|
||||
const errorStatus = profileQuery.error?.status;
|
||||
const error = profileQuery.error;
|
||||
const errorStatus = error && 'status' in error ? (error as { status: number }).status : undefined;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (errorStatus === 401) {
|
||||
|
||||
Reference in New Issue
Block a user