fix oauth
This commit is contained in:
@@ -7,6 +7,15 @@ import { Loader2, ShieldCheck } from 'lucide-react';
|
||||
import { BrandLogo } from '@/components/id/brand-logo';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { approveOAuthAuthorization, fetchAuthSession } from '@/lib/api';
|
||||
|
||||
function buildOAuthQuery(searchParams: URLSearchParams) {
|
||||
const params = new URLSearchParams();
|
||||
searchParams.forEach((value, key) => {
|
||||
if (key !== 'userId') params.set(key, value);
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
function OAuthAuthorizeContent() {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -14,49 +23,59 @@ function OAuthAuthorizeContent() {
|
||||
const { user, token, isPinLocked, isLoading } = useAuth();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [sessionChecked, setSessionChecked] = useState(false);
|
||||
|
||||
const oauthQuery = useMemo(() => {
|
||||
const params = new URLSearchParams();
|
||||
searchParams.forEach((value, key) => params.set(key, value));
|
||||
return params;
|
||||
}, [searchParams]);
|
||||
const oauthQuery = useMemo(() => buildOAuthQuery(searchParams), [searchParams]);
|
||||
|
||||
const clientId = searchParams.get('client_id') ?? searchParams.get('clientId');
|
||||
const redirectUri = searchParams.get('redirect_uri') ?? searchParams.get('redirectUri');
|
||||
const scope = searchParams.get('scope') ?? 'openid profile';
|
||||
const state = searchParams.get('state');
|
||||
|
||||
const returnUrl = useMemo(() => `/auth/oauth/authorize?${oauthQuery.toString()}`, [oauthQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchParams.has('userId')) return;
|
||||
router.replace(returnUrl);
|
||||
}, [returnUrl, router, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
if (!clientId || !redirectUri) return;
|
||||
if (user && token && !isPinLocked) return;
|
||||
const returnUrl = `/auth/oauth/authorize?${oauthQuery.toString()}`;
|
||||
if (isPinLocked) return;
|
||||
if (user && token) return;
|
||||
router.replace(`/auth/login?redirect=${encodeURIComponent(returnUrl)}`);
|
||||
}, [clientId, isLoading, isPinLocked, oauthQuery, redirectUri, router, token, user]);
|
||||
}, [clientId, isLoading, isPinLocked, redirectUri, returnUrl, router, token, user]);
|
||||
|
||||
const approve = useCallback(async () => {
|
||||
if (!token || !user) return;
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
const apiBase = (process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000').replace(/\/$/, '');
|
||||
const url = new URL(`${apiBase}/oauth/authorize`);
|
||||
oauthQuery.forEach((value, key) => url.searchParams.set(key, value));
|
||||
url.searchParams.set('userId', user.id);
|
||||
useEffect(() => {
|
||||
if (isLoading || isPinLocked || !token || !user) {
|
||||
setSessionChecked(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json'
|
||||
let cancelled = false;
|
||||
void fetchAuthSession(token)
|
||||
.then(() => {
|
||||
if (!cancelled) setSessionChecked(true);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setSessionChecked(false);
|
||||
router.replace(`/auth/login?redirect=${encodeURIComponent(returnUrl)}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = (await response.json().catch(() => null)) as { message?: string | string[] } | null;
|
||||
const message = Array.isArray(payload?.message) ? payload.message.join(', ') : payload?.message;
|
||||
throw new Error(message || 'Не удалось подтвердить доступ');
|
||||
}
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isLoading, isPinLocked, returnUrl, router, token, user]);
|
||||
|
||||
const data = (await response.json()) as { redirectUrl?: string };
|
||||
const approve = useCallback(async () => {
|
||||
if (!token || !user || isPinLocked) return;
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await approveOAuthAuthorization(oauthQuery, token);
|
||||
if (!data.redirectUrl) {
|
||||
throw new Error('Сервер не вернул redirect URL');
|
||||
}
|
||||
@@ -66,9 +85,27 @@ function OAuthAuthorizeContent() {
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}, [oauthQuery, token, user]);
|
||||
}, [isPinLocked, oauthQuery, token, user]);
|
||||
|
||||
if (isLoading) {
|
||||
const cancel = useCallback(() => {
|
||||
if (!redirectUri) {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = new URL(redirectUri);
|
||||
url.searchParams.set('error', 'access_denied');
|
||||
url.searchParams.set('error_description', 'Пользователь отклонил запрос');
|
||||
if (state) url.searchParams.set('state', state);
|
||||
window.location.href = url.toString();
|
||||
} catch {
|
||||
router.push('/');
|
||||
}
|
||||
}, [redirectUri, router, state]);
|
||||
|
||||
const authReady = Boolean(user && token && !isPinLocked && sessionChecked);
|
||||
|
||||
if (isLoading || (clientId && redirectUri && !authReady && !isPinLocked)) {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-[#667085]" />
|
||||
@@ -76,6 +113,14 @@ function OAuthAuthorizeContent() {
|
||||
);
|
||||
}
|
||||
|
||||
if (isPinLocked) {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center px-4 text-center">
|
||||
<p className="text-sm text-[#667085]">Подтвердите PIN-код, чтобы продолжить авторизацию приложения.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!clientId || !redirectUri) {
|
||||
return (
|
||||
<div className="mx-auto max-w-md px-4 py-16 text-center">
|
||||
@@ -88,14 +133,6 @@ function OAuthAuthorizeContent() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!user || !token || isPinLocked) {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-[#667085]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex min-h-[70vh] max-w-lg flex-col justify-center px-4 py-12">
|
||||
<div className="mb-8 flex justify-center">
|
||||
@@ -111,7 +148,7 @@ function OAuthAuthorizeContent() {
|
||||
</p>
|
||||
<div className="mt-4 space-y-2 rounded-2xl bg-[#f4f5f8] p-4 text-sm">
|
||||
<p>
|
||||
<span className="text-[#667085]">Пользователь:</span> {user.displayName}
|
||||
<span className="text-[#667085]">Пользователь:</span> {user?.displayName}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-[#667085]">Scopes:</span> {scope}
|
||||
@@ -122,11 +159,11 @@ function OAuthAuthorizeContent() {
|
||||
</div>
|
||||
{error ? <p className="mt-4 text-sm text-red-600">{error}</p> : null}
|
||||
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
||||
<Button className="flex-1 rounded-xl" disabled={submitting} onClick={() => void approve()}>
|
||||
<Button className="flex-1 rounded-xl" disabled={submitting || !authReady} onClick={() => void approve()}>
|
||||
{submitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
Разрешить
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1 rounded-xl" disabled={submitting} onClick={() => router.push('/')}>
|
||||
<Button variant="outline" className="flex-1 rounded-xl" disabled={submitting} onClick={cancel}>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -407,6 +407,18 @@ export async function fetchAuthSession(token?: string | null): Promise<AuthSessi
|
||||
return apiFetch<AuthSessionResponse>('/auth/session', {}, token);
|
||||
}
|
||||
|
||||
export async function approveOAuthAuthorization(params: URLSearchParams, token: string) {
|
||||
const query = new URLSearchParams();
|
||||
params.forEach((value, key) => {
|
||||
if (key !== 'userId') query.set(key, value);
|
||||
});
|
||||
return apiFetch<{ redirectUrl?: string }>(
|
||||
`/oauth/authorize?${query.toString()}`,
|
||||
{ headers: { Accept: 'application/json' } },
|
||||
token
|
||||
);
|
||||
}
|
||||
|
||||
export async function refreshAuthSession(): Promise<RefreshSessionResponse> {
|
||||
if (typeof window === 'undefined') {
|
||||
throw new ApiError('Refresh token недоступен на сервере', 401, 'NO_REFRESH');
|
||||
|
||||
Reference in New Issue
Block a user