Phase 8
This commit is contained in:
82 files changed
+4903
-287
No files matched your search
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user