Harden auth, payments, and frontend against review findings.

Close exploitable gaps in booking/payment flows, enforce token versioning and account checks, gate sensitive payment data, and add middleware plus input validation across admin routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-06-24 19:59:02 +00:00
co-authored by Cursor
parent fc4af38e8a
commit a6840ea953
37 changed files with 1432 additions and 528 deletions
@@ -7,6 +7,7 @@ import { useLanguage } from '@/context/LanguageContext';
import { useAuth } from '@/context/AuthContext';
import { eventsApi, ticketsApi, paymentOptionsApi, Event, PaymentOptionsConfig } from '@/lib/api';
import { formatPrice, formatDateLong, formatTime, getTpagoLink } from '@/lib/utils';
import { isSafeExternalUrl } from '@/lib/safeRedirect';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
@@ -155,8 +156,12 @@ export default function BookingPage() {
return;
}
// Redirect to external booking if enabled
if (eventRes.event.externalBookingEnabled && eventRes.event.externalBookingUrl) {
// Redirect to external booking if enabled (only https:// targets are allowed)
if (
eventRes.event.externalBookingEnabled &&
eventRes.event.externalBookingUrl &&
isSafeExternalUrl(eventRes.event.externalBookingUrl)
) {
window.location.href = eventRes.event.externalBookingUrl;
return;
}
@@ -473,6 +478,16 @@ export default function BookingPage() {
paymentMethod: formData.paymentMethod,
ticketCount,
});
// Fetch full payment credentials now that we hold a ticket capability token
try {
const { paymentOptions } = await paymentOptionsApi.getForEvent(
params.eventId as string,
primaryTicket.id
);
setPaymentConfig(paymentOptions);
} catch {
// Keep the flags-only config from initial load if the gated fetch fails
}
setStep('manual_payment');
} else {
// Cash payment - go straight to success
@@ -69,7 +69,7 @@ export default function BookingPaymentPage() {
// Get payment config for the event
if (ticketData.eventId) {
const { paymentOptions } = await paymentOptionsApi.getForEvent(ticketData.eventId);
const { paymentOptions } = await paymentOptionsApi.getForEvent(ticketData.eventId, ticketData.id);
setPaymentConfig(paymentOptions);
}
+3 -2
View File
@@ -10,6 +10,7 @@ import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import GoogleSignInButton from '@/components/GoogleSignInButton';
import { authApi } from '@/lib/api';
import { safeInternalPath } from '@/lib/safeRedirect';
import toast from 'react-hot-toast';
function LoginContent() {
@@ -25,8 +26,8 @@ function LoginContent() {
password: '',
});
// Check for redirect after login
const redirectTo = searchParams.get('redirect') || '/dashboard';
// Check for redirect after login (only same-origin relative paths are honoured)
const redirectTo = safeInternalPath(searchParams.get('redirect'), '/dashboard');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();