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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1047,6 +1047,7 @@ export default function AdminEmailsPage() {
|
||||
<div className="flex-1 overflow-auto">
|
||||
<iframe
|
||||
srcDoc={previewHtml}
|
||||
sandbox=""
|
||||
className="w-full h-full min-h-[500px]"
|
||||
title="Email Preview"
|
||||
/>
|
||||
@@ -1100,6 +1101,7 @@ export default function AdminEmailsPage() {
|
||||
{selectedLog.bodyHtml ? (
|
||||
<iframe
|
||||
srcDoc={selectedLog.bodyHtml}
|
||||
sandbox=""
|
||||
className="w-full h-full min-h-[400px]"
|
||||
title="Email Content"
|
||||
/>
|
||||
|
||||
@@ -2217,7 +2217,7 @@ export default function AdminEventDetailPage() {
|
||||
<Button variant="outline" size="sm" onClick={() => setPreviewHtml(null)} className="min-h-[44px] md:min-h-0">Close</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<iframe srcDoc={previewHtml} className="w-full h-full min-h-[500px]" title="Email Preview" />
|
||||
<iframe srcDoc={previewHtml} sandbox="" className="w-full h-full min-h-[500px]" title="Email Preview" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { timingSafeEqual } from 'crypto';
|
||||
|
||||
// Constant-time string comparison to avoid leaking the secret via response timing.
|
||||
function secretsMatch(provided: unknown, expected: string): boolean {
|
||||
if (typeof provided !== 'string' || provided.length === 0) return false;
|
||||
const a = Buffer.from(provided);
|
||||
const b = Buffer.from(expected);
|
||||
if (a.length !== b.length) return false;
|
||||
return timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { secret, tag } = body;
|
||||
|
||||
// Validate the revalidation secret
|
||||
// Validate the revalidation secret. Reject if it is unset or left at an insecure default.
|
||||
const revalidateSecret = process.env.REVALIDATE_SECRET;
|
||||
if (!revalidateSecret || secret !== revalidateSecret) {
|
||||
if (!revalidateSecret || revalidateSecret === 'change-me' || revalidateSecret.length < 16) {
|
||||
return NextResponse.json({ error: 'Revalidation is not configured' }, { status: 503 });
|
||||
}
|
||||
if (!secretsMatch(secret, revalidateSecret)) {
|
||||
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user