Files
inbuxa-admin/src/pages/OAuthCallback.tsx
T

106 lines
3.7 KiB
TypeScript

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Loader2 } from 'lucide-react';
import { useAuthStore } from '@/stores/authStore';
import { getBasePath } from '@/lib/basePath';
import { exchangeCode, getStoredOAuthData, clearStoredOAuthData, getOAuthRedirectUri } from '@/services/auth/oauth';
export default function OAuthCallback() {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function handleCallback() {
try {
const code = searchParams.get('code');
const stateParam = searchParams.get('state');
const errorParam = searchParams.get('error');
const errorDescription = searchParams.get('error_description');
if (errorParam) {
throw new Error(errorDescription || errorParam);
}
if (!code || !stateParam) {
throw new Error(t('oauth.missingParams', 'Missing authorization code or state parameter'));
}
const { codeVerifier, tokenEndpoint, state, returnUrl, endSessionEndpoint } = getStoredOAuthData();
if (!state || state !== stateParam) {
throw new Error(t('oauth.stateMismatch', 'State parameter mismatch. Please try logging in again.'));
}
if (!codeVerifier || !tokenEndpoint) {
throw new Error(t('oauth.missingData', 'Missing OAuth session data. Please try logging in again.'));
}
const redirectUri = getOAuthRedirectUri();
const tokenResponse = await exchangeCode(code, codeVerifier, tokenEndpoint, redirectUri);
useAuthStore
.getState()
.setTokens(
tokenResponse.access_token,
tokenResponse.refresh_token,
tokenResponse.expires_in,
tokenEndpoint,
endSessionEndpoint,
);
clearStoredOAuthData();
const basePath = getBasePath();
const stripBase = (p: string) => (basePath && p.startsWith(basePath) ? p.slice(basePath.length) || '/' : p);
const candidate = returnUrl ? stripBase(returnUrl) : '/';
const isAuthPath =
candidate === '/login' ||
candidate.startsWith('/login?') ||
candidate === '/oauth/callback' ||
candidate.startsWith('/oauth/callback?');
const destination = isAuthPath ? '/' : candidate;
navigate(destination, { replace: true });
} catch (err) {
clearStoredOAuthData();
setError(err instanceof Error ? err.message : t('oauth.error', 'Authentication failed'));
}
}
handleCallback();
}, [navigate, searchParams, t]);
if (error) {
return (
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="w-full max-w-sm space-y-4 text-center">
<p className="text-sm text-destructive" role="alert">
{error}
</p>
<a href={`${getBasePath()}/login`} className="text-sm text-primary underline-offset-4 hover:underline">
{t('oauth.backToLogin', 'Back to login')}
</a>
</div>
</div>
);
}
return (
<div className="flex min-h-screen items-center justify-center bg-background">
<div className="flex flex-col items-center gap-3">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<p className="text-sm text-muted-foreground">{t('oauth.processing', 'Completing sign in...')}</p>
</div>
</div>
);
}