From 21f2a1c2277787357b38aef47fb87115e7284274 Mon Sep 17 00:00:00 2001 From: lendry Date: Wed, 24 Jun 2026 17:53:58 +0300 Subject: [PATCH] fix idp on started --- .env.example | 2 + apps/frontend/Dockerfile | 4 + apps/frontend/app/auth/register/page.tsx | 182 ++++++++++++++++++----- apps/frontend/lib/api.ts | 12 +- apps/frontend/next.config.ts | 22 ++- docker-compose.yml | 4 + install.sh | 35 +++-- 7 files changed, 211 insertions(+), 50 deletions(-) diff --git a/.env.example b/.env.example index 2ff71e1..2112a4a 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,8 @@ PUBLIC_API_URL=http://localhost:3000 PUBLIC_FRONTEND_URL=http://localhost:3002 PUBLIC_DOCS_URL=http://localhost:3003 PUBLIC_WS_URL=ws://localhost:8085/ws +INTERNAL_API_URL=http://api-gateway:3000 +INTERNAL_WS_URL=http://media-ws:8085 # Nginx (USE_NGINX_SSL=true только при SSL_TYPE=letsencrypt|selfsigned) USE_NGINX_SSL=false diff --git a/apps/frontend/Dockerfile b/apps/frontend/Dockerfile index 49e04cd..662ea97 100644 --- a/apps/frontend/Dockerfile +++ b/apps/frontend/Dockerfile @@ -7,6 +7,8 @@ WORKDIR /app ARG NPM_REGISTRY=https://registry.npmjs.org ARG NEXT_PUBLIC_API_URL=http://localhost:3000 ARG NEXT_PUBLIC_WS_URL=ws://localhost:8085/ws +ARG INTERNAL_API_URL=http://api-gateway:3000 +ARG INTERNAL_WS_URL=http://media-ws:8085 COPY package.json package-lock.json .npmrc ./ COPY apps/sso-core/package.json ./apps/sso-core/ @@ -34,6 +36,8 @@ ENV PORT="3000" ENV HOSTNAME="0.0.0.0" ENV NEXT_PUBLIC_API_URL="${NEXT_PUBLIC_API_URL}" ENV NEXT_PUBLIC_WS_URL="${NEXT_PUBLIC_WS_URL}" +ENV INTERNAL_API_URL="${INTERNAL_API_URL}" +ENV INTERNAL_WS_URL="${INTERNAL_WS_URL}" RUN npm --workspace @lendry/frontend run build diff --git a/apps/frontend/app/auth/register/page.tsx b/apps/frontend/app/auth/register/page.tsx index 86aede6..df8ad2a 100644 --- a/apps/frontend/app/auth/register/page.tsx +++ b/apps/frontend/app/auth/register/page.tsx @@ -3,33 +3,59 @@ import Link from 'next/link'; import { FormEvent, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { ShieldCheck } from 'lucide-react'; +import { ChevronLeft, UserRound } from 'lucide-react'; import { BrandLogo } from '@/components/id/brand-logo'; import { useAuth } from '@/components/id/auth-provider'; import { useToast } from '@/components/id/toast-provider'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { OtpInput } from '@/components/ui/otp-input'; import { PhoneInput, phoneCountries, toE164 } from '@/components/ui/phone-input'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +type Step = 'contact' | 'otp' | 'pin'; + export default function RegisterPage() { const router = useRouter(); - const { sendLoginOtp } = useAuth(); + const { sendLoginOtp, verifyLoginOtp, completePin } = useAuth(); const { showToast } = useToast(); const [authTab, setAuthTab] = useState<'email' | 'phone'>('email'); const [email, setEmail] = useState(''); const [phoneNumber, setPhoneNumber] = useState(''); const [country, setCountry] = useState(phoneCountries[0]); + const [recipient, setRecipient] = useState(''); + const [maskedTarget, setMaskedTarget] = useState(''); + const [otp, setOtp] = useState(''); + const [pin, setPin] = useState(''); + const [pendingSessionId, setPendingSessionId] = useState(null); + const [step, setStep] = useState('contact'); const [isSubmitting, setIsSubmitting] = useState(false); - async function handleSubmit(event: FormEvent) { + function getIdentifier() { + return authTab === 'email' ? email.trim() : toE164(country, phoneNumber); + } + + function finishRegistration(pinVerified: boolean, sessionId: string) { + if (!pinVerified) { + setPendingSessionId(sessionId); + setStep('pin'); + return; + } + showToast('Добро пожаловать! ID успешно создан.'); + router.push('/'); + } + + async function handleSendCode(event: FormEvent) { event.preventDefault(); setIsSubmitting(true); try { - const recipient = authTab === 'email' ? email.trim() : toE164(country, phoneNumber); - await sendLoginOtp(recipient); - showToast('Код отправлен. Подтвердите его на экране входа.'); - router.push('/auth/login'); + const identifier = getIdentifier(); + setRecipient(identifier); + const masked = await sendLoginOtp(identifier); + setMaskedTarget(masked); + setOtp(''); + setStep('otp'); + showToast('Код отправлен. Введите его ниже.'); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось отправить код'); } finally { @@ -37,41 +63,129 @@ export default function RegisterPage() { } } + async function verifyOtp(code: string) { + setIsSubmitting(true); + try { + const response = await verifyLoginOtp(recipient, code); + if (response.auth) { + finishRegistration(response.auth.pinVerified, response.auth.sessionId); + } else { + showToast('Не удалось завершить регистрацию'); + } + } catch (error) { + setOtp(''); + showToast(error instanceof Error ? error.message : 'Неверный код'); + } finally { + setIsSubmitting(false); + } + } + + async function handlePinSubmit(event: FormEvent) { + event.preventDefault(); + if (!pendingSessionId) return; + setIsSubmitting(true); + try { + await completePin(pendingSessionId, pin); + showToast('Добро пожаловать! ID успешно создан.'); + router.push('/'); + } catch (error) { + showToast(error instanceof Error ? error.message : 'Не удалось проверить PIN-код'); + } finally { + setIsSubmitting(false); + } + } + + function resetToContact() { + setStep('contact'); + setOtp(''); + setPin(''); + setPendingSessionId(null); + } + return (

Создайте ID

-

Введите почту или телефон. Пароль не нужен.

+

+ {step === 'contact' ? 'Введите почту или телефон. Пароль не нужен.' : 'Подтвердите код — аккаунт создастся автоматически.'} +

-
- setAuthTab(value as 'email' | 'phone')} className="w-full"> - - Почта - Телефон - - - setEmail(event.target.value)} - required={authTab === 'email'} - /> - - - - - - + ) : null} + + {step === 'contact' ? ( + + setAuthTab(value as 'email' | 'phone')} className="w-full"> + + Почта + Телефон + + + setEmail(event.target.value)} + required={authTab === 'email'} + /> + + + + + + +
+ ) : null} + + {step === 'otp' ? ( +
+
+
+ +
+
+

Код отправлен на

+

{maskedTarget || recipient}

+
+
+ + {isSubmitting ?

Создаём ваш ID...

: null} +
+ ) : null} + + {step === 'pin' && pendingSessionId ? ( +
+

Установите PIN для завершения регистрации

+ setPin(event.target.value)} + required + minLength={4} + maxLength={6} + /> + +
+ ) : null} + + {step === 'contact' ? ( + - - + ) : null}
); diff --git a/apps/frontend/lib/api.ts b/apps/frontend/lib/api.ts index ecb8c37..ace7de6 100644 --- a/apps/frontend/lib/api.ts +++ b/apps/frontend/lib/api.ts @@ -9,8 +9,8 @@ function resolveBrowserApiBaseUrl(): string { try { const configured = new URL(configuredApiUrl); - if (configured.origin !== window.location.origin) { - return `${window.location.origin}${API_ORIGIN_PROXY_PREFIX}`; + if (configured.hostname !== window.location.hostname) { + return API_ORIGIN_PROXY_PREFIX; } } catch { // keep configured URL @@ -24,9 +24,15 @@ function resolveBrowserWsBaseUrl(): string { return configuredWsUrl; } + const apiBase = resolveBrowserApiBaseUrl(); + if (apiBase === API_ORIGIN_PROXY_PREFIX) { + const scheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${scheme}//${window.location.host}${API_ORIGIN_PROXY_PREFIX}/ws`; + } + try { const configured = new URL(configuredWsUrl); - if (configured.origin !== window.location.origin) { + if (configured.hostname !== window.location.hostname) { const scheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${scheme}//${window.location.host}${API_ORIGIN_PROXY_PREFIX}/ws`; } diff --git a/apps/frontend/next.config.ts b/apps/frontend/next.config.ts index 09aacf2..9be15d2 100644 --- a/apps/frontend/next.config.ts +++ b/apps/frontend/next.config.ts @@ -1,7 +1,27 @@ import type { NextConfig } from 'next'; +const internalApiUrl = ( + process.env.INTERNAL_API_URL ?? + process.env.NEXT_PUBLIC_API_URL ?? + 'http://localhost:3000' +).replace(/\/$/, ''); + +const internalWsUrl = (process.env.INTERNAL_WS_URL ?? 'http://media-ws:8085').replace(/\/$/, ''); + const nextConfig: NextConfig = { - typedRoutes: false + typedRoutes: false, + async rewrites() { + return [ + { + source: '/idp-api/ws', + destination: `${internalWsUrl}/ws` + }, + { + source: '/idp-api/:path*', + destination: `${internalApiUrl}/:path*` + } + ]; + } }; export default nextConfig; diff --git a/docker-compose.yml b/docker-compose.yml index 21e1af7..f793082 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -201,6 +201,8 @@ services: NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org} NEXT_PUBLIC_API_URL: ${PUBLIC_API_URL:-http://localhost:3000} NEXT_PUBLIC_WS_URL: ${PUBLIC_WS_URL:-ws://localhost:8085/ws} + INTERNAL_API_URL: ${INTERNAL_API_URL:-http://api-gateway:3000} + INTERNAL_WS_URL: ${INTERNAL_WS_URL:-http://media-ws:8085} container_name: lendry-id-frontend restart: unless-stopped environment: @@ -209,6 +211,8 @@ services: NEXT_TELEMETRY_DISABLED: 1 NEXT_PUBLIC_API_URL: ${PUBLIC_API_URL:-http://localhost:3000} NEXT_PUBLIC_WS_URL: ${PUBLIC_WS_URL:-ws://localhost:8085/ws} + INTERNAL_API_URL: ${INTERNAL_API_URL:-http://api-gateway:3000} + INTERNAL_WS_URL: ${INTERNAL_WS_URL:-http://media-ws:8085} ports: - "127.0.0.1:3002:3000" depends_on: diff --git a/install.sh b/install.sh index c27521b..3d1e740 100644 --- a/install.sh +++ b/install.sh @@ -10,7 +10,7 @@ set -euo pipefail -SCRIPT_VERSION="2.2.2" +SCRIPT_VERSION="2.2.3" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT_DIR" @@ -1323,11 +1323,12 @@ EOF } build_nginx_idp_api_proxy_block() { - local api_upstream + local api_upstream ws_upstream api_upstream="$(nginx_upstream api)" + ws_upstream="$(nginx_upstream ws)" cat <