- ITN: XML w base64 (pole transactions), bezpieczny parser (bez XXE), podpis ze stalej listy pol, zgodnosc ServiceID, kwoty i waluty, idempotencja, podpisana odpowiedz XML CONFIRMED/NOTCONFIRMED. - Start transakcji podpisanym formularzem POST ze strony /platnosc/:orderId zamiast linku GET z podpisem w adresie; tytul przelewu w ASCII. - Losowy OrderID (UUID) zamiast Math.random. - Adres ITN do wpisania w panelu AutoPay widoczny w panelu admina. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ import { AdminPromotionView } from './admin/promotion';
|
||||
import { AdminSettingsView } from './admin/settings';
|
||||
import { TermsPage, PrivacyPolicyPage, CookieBanner } from './legal';
|
||||
import { SiteFooter } from './Footer';
|
||||
import { ActivationPage, PasswordResetPage, PhoneOtpModal } from './AuthPages';
|
||||
import { ActivationPage, PasswordResetPage, PaymentRedirectPage, PhoneOtpModal } from './AuthPages';
|
||||
|
||||
const heroImage = new URL('./assets/hero-interior.png', import.meta.url).href;
|
||||
const cityImage = new URL('./assets/city-panorama.png', import.meta.url).href;
|
||||
@@ -3668,6 +3668,7 @@ function App() {
|
||||
|
||||
{/* Trasy wymagajace zalogowania */}
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path={ROUTES.payment} element={<PaymentRedirectPage />} />
|
||||
<Route path={ROUTES.guideAdd} element={<GuideAddArticlePage />} />
|
||||
<Route path={ROUTES.account} element={<AccountDashboardPage savedSearches={savedSearchesState} onOpenSavedSearch={openSavedSearch} />} />
|
||||
<Route
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useEffect, useRef, useState, type FormEvent, type ReactNode } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { ROUTES } from './routes';
|
||||
import { useAuth } from './auth';
|
||||
import { apiFetch, useAuth } from './auth';
|
||||
|
||||
const errMsg = (e: unknown) => (e instanceof Error ? e.message : 'Wystąpił błąd. Spróbuj ponownie.');
|
||||
|
||||
@@ -65,6 +65,76 @@ export function ActivationPage() {
|
||||
);
|
||||
}
|
||||
|
||||
type AutoPayStartForm = {
|
||||
gatewayUrl: string;
|
||||
serviceId: string;
|
||||
orderId: string;
|
||||
amount: string;
|
||||
description: string;
|
||||
customerEmail: string | null;
|
||||
hash: string;
|
||||
};
|
||||
|
||||
// /platnosc/:orderId - przekierowanie do bramki AutoPay. Transakcje startuje sie formularzem POST,
|
||||
// wiec podpisane pola pobieramy z API i wysylamy automatycznie. Podpis nie trafia do paska adresu,
|
||||
// historii przegladarki ani naglowka Referer.
|
||||
export function PaymentRedirectPage() {
|
||||
const { orderId } = useParams<{ orderId: string }>();
|
||||
const [form, setForm] = useState<AutoPayStartForm | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const ran = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (ran.current) {
|
||||
return;
|
||||
}
|
||||
ran.current = true;
|
||||
apiFetch<AutoPayStartForm>(`/promotion/order/${encodeURIComponent(orderId ?? '')}/autopay-form`)
|
||||
.then(setForm)
|
||||
.catch((e) => setError(errMsg(e)));
|
||||
}, [orderId]);
|
||||
|
||||
// Automatyczna wysylka formularza po pobraniu podpisanych pol.
|
||||
useEffect(() => {
|
||||
if (form) {
|
||||
formRef.current?.submit();
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<AuthStatusCard>
|
||||
<div className="auth-status-icon err">!</div>
|
||||
<h1>Nie udało się rozpocząć płatności</h1>
|
||||
<p className="auth-status-lead">{error}</p>
|
||||
<Link className="auth-status-btn" to={ROUTES.accountListings}>Wróć do moich ofert</Link>
|
||||
</AuthStatusCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthStatusCard>
|
||||
<h1>Przekierowanie do płatności</h1>
|
||||
<p className="auth-status-lead">
|
||||
{form ? 'Za chwilę nastąpi przekierowanie do AutoPay...' : 'Przygotowujemy bezpieczną płatność...'}
|
||||
</p>
|
||||
{form && (
|
||||
<form ref={formRef} method="POST" action={form.gatewayUrl}>
|
||||
<input type="hidden" name="ServiceID" value={form.serviceId} />
|
||||
<input type="hidden" name="OrderID" value={form.orderId} />
|
||||
<input type="hidden" name="Amount" value={form.amount} />
|
||||
<input type="hidden" name="Description" value={form.description} />
|
||||
{form.customerEmail && <input type="hidden" name="CustomerEmail" value={form.customerEmail} />}
|
||||
<input type="hidden" name="Hash" value={form.hash} />
|
||||
{/* Zapasowy przycisk, gdyby automatyczna wysylka nie zadzialala. */}
|
||||
<button className="auth-status-btn" type="submit">Kontynuuj do płatności</button>
|
||||
</form>
|
||||
)}
|
||||
</AuthStatusCard>
|
||||
);
|
||||
}
|
||||
|
||||
// /reset-hasla/:token - ustawienie nowego hasla.
|
||||
export function PasswordResetPage() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
|
||||
@@ -217,7 +217,7 @@ export function AdminPromotionView({ Icon }: { Icon: IconComponent }) {
|
||||
<span className="mkt-config-icon"><Icon name="credit-card" /></span>
|
||||
<h2>Bramka szybkich płatności - AutoPay</h2>
|
||||
</div>
|
||||
<p className="banner-slot-hint"><Icon name="lock" /> Bez danych dostępowych i włączonej bramki zakup promowania jest niedostępny.</p>
|
||||
<p className="banner-slot-hint"><Icon name="lock" /> Bez danych dostępowych i włączonej bramki zakup promowania jest niedostępny. Adres ITN w panelu AutoPay (Ustawienia serwisu → Konfiguracja techniczna → Edytuj): {window.location.origin}/api/payments/autopay/itn</p>
|
||||
<div className="mkt-config-grid">
|
||||
<label>ServiceID
|
||||
<input value={pay.serviceId} onChange={(e) => setPay({ ...pay, serviceId: e.target.value })} placeholder="np. 123456" />
|
||||
|
||||
@@ -54,6 +54,8 @@ export const ROUTES = {
|
||||
privacy: '/polityka-prywatnosci',
|
||||
activation: '/aktywacja/:token',
|
||||
passwordReset: '/reset-hasla/:token',
|
||||
// Przekierowanie do bramki AutoPay (podpisany formularz POST) dla zamowienia promowania.
|
||||
payment: '/platnosc/:orderId',
|
||||
notifications: '/powiadomienia',
|
||||
favorites: '/ulubione',
|
||||
messages: '/wiadomosci',
|
||||
@@ -123,6 +125,11 @@ export function newConversationPath(listingId: number): string {
|
||||
return `/wiadomosci/nowa/${listingId}`;
|
||||
}
|
||||
|
||||
// Przekierowanie do bramki platnosci dla zamowienia promowania (adres zwraca tez backend przy checkout).
|
||||
export function paymentPath(orderId: number): string {
|
||||
return `/platnosc/${orderId}`;
|
||||
}
|
||||
|
||||
// Dedykowana strona edycji wlasnego ogloszenia.
|
||||
export function listingEditPath(id: number): string {
|
||||
return `/edytuj-ogloszenie/${id}`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { ROUTES, negotiationPath, ADMIN_CONVERSATION_SLUG, conversationPath, newConversationPath } from '../src/routes';
|
||||
import { ROUTES, negotiationPath, ADMIN_CONVERSATION_SLUG, conversationPath, newConversationPath, paymentPath } from '../src/routes';
|
||||
|
||||
// Standard projektu (AGENTS.md): kazdy widok ma wlasny URL obslugiwany przez React Router,
|
||||
// a nawigacja odbywa sie przez <Link>/<NavLink>. Te testy pilnuja tej zasady.
|
||||
@@ -34,6 +34,7 @@ const PROTECTED_ROUTES = [
|
||||
ROUTES.messages,
|
||||
conversationPath(ADMIN_CONVERSATION_SLUG),
|
||||
newConversationPath(1),
|
||||
paymentPath(1),
|
||||
ROUTES.add,
|
||||
ROUTES.admin,
|
||||
`${ROUTES.admin}/leady`,
|
||||
|
||||
Reference in New Issue
Block a user