Initial commit
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 } = 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);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user