This commit is contained in:
Stefan Lazic committed 2026-08-04 09:57:58 +01:00
1 parent 546613e677
commit 4785510144
82 files changed
+4903 -287

No files matched your search

+16
View File
@@ -8,6 +8,7 @@ import { checkRateLimit } from "@/lib/security/rate-limit";
import { getRequestIp } from "@/lib/security/request-ip";
import { resolveCustomerForBooking } from "@/services/customer/customer.service";
import { createBooking } from "@/services/scheduling/booking.service";
import { syncAppointmentSafely } from "@/services/calendar/calendar-sync.service";
import { getFirstFriendlyMessage } from "../error-messages";
export interface ConfirmBookingResult {
@@ -44,6 +45,16 @@ export async function confirmBookingAction(businessId: string): Promise<ConfirmB
if (!draft.guest) {
return { success: false, error: "Please provide your contact details first." };
}
// Authoritative guest-booking gate — the UI already hides the
// guest path when disabled (see /book/details), but this is the
// one place that actually creates an Appointment row, so it's
// re-checked here regardless of how the request arrived (a direct
// Server Action call, a stale draft cookie started before the
// owner disabled guest booking mid-flow, etc.).
const bookingRule = await prisma.bookingRule.findUnique({ where: { businessId }, select: { allowGuestBooking: true } });
if (!bookingRule?.allowGuestBooking) {
return { success: false, error: "Guest booking is currently unavailable. Please sign in or create an account to continue." };
}
// Registered customers already have a rate-limited signin gate on
// their account; a guest has none, so this is the one place spam
// bookings can otherwise be generated with no account at all.
@@ -72,6 +83,11 @@ export async function confirmBookingAction(businessId: string): Promise<ConfirmB
return { success: false, error: getFirstFriendlyMessage(result.validation) };
}
// A no-op when the appointment is PENDING (requires approval) — see
// calendar-sync.service.ts's own policy comment. Never blocks or fails
// this action either way.
await syncAppointmentSafely(prisma, result.appointment.id);
await writeLastBookingReference({
appointmentId: result.appointment.id,
guestAccessToken: result.guestAccessToken,
@@ -5,6 +5,7 @@ import { auth } from "@/auth";
import { prisma } from "@/lib/db/prisma";
import { getAppointmentById } from "@/services/scheduling/appointment.service";
import { cancelBooking, rescheduleBooking } from "@/services/scheduling/booking.service";
import { syncAppointmentSafely } from "@/services/calendar/calendar-sync.service";
import { assertCanAccessAppointment, PermissionDeniedError } from "../authorization";
import { getFirstFriendlyMessage } from "../error-messages";
import { customerCancelSchema, customerRescheduleSchema } from "../schemas";
@@ -50,6 +51,8 @@ export async function cancelCustomerAppointmentAction(input: unknown): Promise<C
return { success: false, error: getFirstFriendlyMessage(result.validation) };
}
await syncAppointmentSafely(prisma, parsed.data.appointmentId);
// Deliberately not revalidating here (unlike reschedule) — every affected
// page is already `dynamic = "force-dynamic"`, so revalidatePath buys no
// extra freshness, and doing it immediately would drop the cancelled
@@ -87,6 +90,8 @@ export async function rescheduleCustomerAppointmentAction(input: unknown): Promi
return { success: false, error: getFirstFriendlyMessage(result.validation) };
}
await syncAppointmentSafely(prisma, parsed.data.appointmentId);
revalidatePath("/appointments");
revalidatePath(`/appointments/${parsed.data.appointmentId}`);
return { success: true };
+10
View File
@@ -96,6 +96,16 @@ export async function saveDetailsStepAction(input: unknown): Promise<StepActionR
const session = await auth();
if (!session?.user) {
// Same authoritative check confirmBookingAction re-runs at the final
// commit — rejected here too so a direct call to this action (not
// just the confirm step) can't bypass a disabled guest-booking rule.
const bookingRule = await prisma.bookingRule.findUnique({
where: { businessId: parsed.data.businessId },
select: { allowGuestBooking: true },
});
if (!bookingRule?.allowGuestBooking) {
return { success: false, error: "Please sign in or create an account to continue." };
}
const guestParsed = guestDetailsSchema.safeParse({
firstName: parsed.data.firstName,
lastName: parsed.data.lastName,