diff --git a/goauthsocialfrontendtest/app/auth/callback/client.tsx b/goauthsocialfrontendtest/app/auth/callback/client.tsx new file mode 100644 index 0000000..f7f4fe2 --- /dev/null +++ b/goauthsocialfrontendtest/app/auth/callback/client.tsx @@ -0,0 +1,225 @@ +'use client'; + +import Link from "next/link"; +import { useEffect, useMemo, useState } from "react"; +import { useSearchParams } from "next/navigation"; + +import { env } from "@/lib/env"; + +type Status = "loading" | "success" | "error"; + +interface AuthCallbackResponse { + provider: string; + provider_user_id: string; + access_token: string; + refresh_token?: string | null; + expires_in?: number | null; + application_token?: string | null; + raw?: Record; +} + +export function YandexCallbackContent() { + const searchParams = useSearchParams(); + const [status, setStatus] = useState("loading"); + const [message, setMessage] = useState("Проверяем ответ от Yandex ID…"); + const [payload, setPayload] = useState(null); + + const queryEntries = useMemo(() => { + return Object.fromEntries(searchParams.entries()); + }, [searchParams]); + + useEffect(() => { + const error = searchParams.get("error"); + if (error) { + setStatus("error"); + setMessage( + `Яндекс вернул ошибку: ${error}${ + searchParams.get("error_description") + ? ` (${searchParams.get("error_description")})` + : "" + }`, + ); + return; + } + + const code = searchParams.get("code"); + const state = searchParams.get("state"); + + if (!code) { + setStatus("error"); + setMessage("В ответе отсутствует код авторизации (code)."); + return; + } + + if (!env.backendBaseUrl) { + setStatus("error"); + setMessage("Не указан адрес бэкенда (NEXT_PUBLIC_API_BASE_URL)."); + return; + } + + const controller = new AbortController(); + + async function sendCallback() { + try { + setStatus("loading"); + setMessage("Передаём код авторизации на бэкенд…"); + + const response = await fetch( + `${env.backendBaseUrl.replace( + /\/$/, + "", + )}/api/v1/auth/callback/yandex`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + signal: controller.signal, + body: JSON.stringify({ + code, + state, + raw: queryEntries, + }), + }, + ); + + if (!response.ok) { + const text = await response.text(); + throw new Error( + `Бэкенд вернул ${response.status}. Тело ответа: ${text || "пусто"}`, + ); + } + + const data = (await response.json()) as AuthCallbackResponse; + setPayload(data); + setStatus("success"); + setMessage("Авторизация завершена успешно."); + } catch (error) { + console.error("Ошибка при обработке ответа от Яндекс.", error); + setStatus("error"); + setMessage( + error instanceof Error + ? error.message + : "Не удалось обработать ответ от Яндекс.", + ); + } + } + + void sendCallback(); + + return () => controller.abort(); + }, [queryEntries, searchParams]); + + const renderContent = () => { + if (status === "loading") { + return ( +
+
+

{message}

+
+ ); + } + + if (status === "error") { + return ( +
+

Ошибка авторизации

+

{message}

+ + Вернуться и попробовать снова + +
+ ); + } + + return ( +
+
+

+ Готово +

+

Вход выполнен

+

+ Мы сохранили информацию пользователя и токены в бэкенде и вернули + application_token, который можно использовать для дальнейших + запросов. +

+
+ + {payload && ( +
+ + + {payload.application_token && ( + + )} + + {payload.refresh_token && ( + + )} + {typeof payload.expires_in === "number" && ( + + )} +
+ )} + +
+

+ Храните токены в безопасном месте и используйте их для обращения к + API вашего сервиса. Для завершения сессии удалите токены на стороне + клиента и выполните логаут на сервере. +

+ + Вернуться на главную + +
+
+ ); + }; + + return ( +
+
+ {renderContent()} +
+
+ ); +} + +interface FieldProps { + label: string; + value: string; + masked?: boolean; +} + +function Field({ label, value, masked }: FieldProps) { + const displayValue = + masked && value ? `${value.slice(0, 4)}••••${value.slice(-4)}` : value || "—"; + + return ( +
+ + {label} + + {displayValue} +
+ ); +} diff --git a/goauthsocialfrontendtest/app/auth/callback/page.tsx b/goauthsocialfrontendtest/app/auth/callback/page.tsx index e0a480b..d509e30 100644 --- a/goauthsocialfrontendtest/app/auth/callback/page.tsx +++ b/goauthsocialfrontendtest/app/auth/callback/page.tsx @@ -1,217 +1,24 @@ -'use client'; +import { Suspense } from "react"; -import Link from "next/link"; -import { useEffect, useMemo, useState } from "react"; -import { useSearchParams } from "next/navigation"; +import { YandexCallbackContent } from "./client"; -import { env } from "@/lib/env"; - -type Status = "loading" | "success" | "error"; - -interface AuthCallbackResponse { - provider: string; - provider_user_id: string; - access_token: string; - refresh_token?: string | null; - expires_in?: number | null; - application_token?: string | null; - raw?: Record; +function Loading() { + return ( +
+
+
+

+ Загружаем данные авторизации Yandex ID… +

+
+
+ ); } export default function YandexCallbackPage() { - const searchParams = useSearchParams(); - const [status, setStatus] = useState("loading"); - const [message, setMessage] = useState("Проверяем ответ от Yandex ID…"); - const [payload, setPayload] = useState(null); - - const queryEntries = useMemo(() => { - return Object.fromEntries(searchParams.entries()); - }, [searchParams]); - - useEffect(() => { - const error = searchParams.get("error"); - if (error) { - setStatus("error"); - setMessage( - `Яндекс вернул ошибку: ${error}${ - searchParams.get("error_description") - ? ` (${searchParams.get("error_description")})` - : "" - }`, - ); - return; - } - - const code = searchParams.get("code"); - const state = searchParams.get("state"); - - if (!code) { - setStatus("error"); - setMessage("В ответе отсутствует код авторизации (code)."); - return; - } - - if (!env.backendBaseUrl) { - setStatus("error"); - setMessage("Не указан адрес бэкенда (NEXT_PUBLIC_API_BASE_URL)."); - return; - } - - const controller = new AbortController(); - - async function sendCallback() { - try { - setStatus("loading"); - setMessage("Передаём код авторизации на бэкенд…"); - - const response = await fetch( - `${env.backendBaseUrl.replace(/\/$/, "")}/api/v1/auth/callback/yandex`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", - signal: controller.signal, - body: JSON.stringify({ - code, - state, - raw: queryEntries, - }), - }, - ); - - if (!response.ok) { - const text = await response.text(); - throw new Error( - `Бэкенд вернул ${response.status}. Тело ответа: ${text || "пусто"}`, - ); - } - - const data = (await response.json()) as AuthCallbackResponse; - setPayload(data); - setStatus("success"); - setMessage("Авторизация завершена успешно."); - } catch (error) { - console.error("Ошибка при обработке ответа от Яндекс.", error); - setStatus("error"); - setMessage( - error instanceof Error - ? error.message - : "Не удалось обработать ответ от Яндекс.", - ); - } - } - - void sendCallback(); - - return () => controller.abort(); - }, [queryEntries, searchParams]); - - const renderContent = () => { - if (status === "loading") { - return ( -
-
-

{message}

-
- ); - } - - if (status === "error") { - return ( -
-

Ошибка авторизации

-

{message}

- - Вернуться и попробовать снова - -
- ); - } - - return ( -
-
-

Готово

-

Вход выполнен

-

- Мы сохранили информацию пользователя и токены в бэкенде и вернули - application_token, который можно использовать для дальнейших запросов. -

-
- - {payload && ( -
- - - {payload.application_token && ( - - )} - - {payload.refresh_token && ( - - )} - {typeof payload.expires_in === "number" && ( - - )} -
- )} - -
-

- Храните токены в безопасном месте и используйте их для обращения к - API вашего сервиса. Для завершения сессии удалите токены на стороне - клиента и выполните логаут на сервере. -

- - Вернуться на главную - -
-
- ); - }; - - return ( -
-
- {renderContent()} -
-
- ); -} - -interface FieldProps { - label: string; - value: string; - masked?: boolean; -} - -function Field({ label, value, masked }: FieldProps) { - const displayValue = - masked && value - ? `${value.slice(0, 4)}••••${value.slice(-4)}` - : value || "—"; - - return ( -
- - {label} - - {displayValue} -
- ); + return ( + }> + + + ); }