diff --git a/apps/web/src/actions/reminders.ts b/apps/web/src/actions/reminders.ts new file mode 100644 index 0000000..cd48e30 --- /dev/null +++ b/apps/web/src/actions/reminders.ts @@ -0,0 +1,42 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; +import { headers } from "next/headers"; +import { eq } from "drizzle-orm"; +import { reminders } from "@cmbot/db"; +import { db } from "@/lib/db"; +import { getSeededOperator } from "@/lib/operator"; +import { checkRateLimit } from "@/lib/rate-limit"; + +async function rateLimit(key: string) { + const h = await headers(); + const ip = h.get("x-forwarded-for")?.split(",")[0]?.trim() ?? h.get("x-real-ip") ?? "unknown"; + const r = await checkRateLimit(`${key}:${ip}`, { max: 30, windowSec: 10 }); + if (r.limited) throw new Error("Too many requests"); +} + +export async function deleteReminderAction(formData: FormData): Promise { + await rateLimit("delete-reminder"); + const reminderId = formData.get("reminderId"); + if (typeof reminderId !== "string") return; + + const op = await getSeededOperator(); + const reminder = await db.query.reminders.findFirst({ + where: (r, { eq }) => eq(r.id, reminderId), + }); + if (!reminder) return; + + // Verify ownership via the account + const account = await db.query.whatsappAccounts.findFirst({ + where: (a, { eq, and }) => and(eq(a.id, reminder.accountId), eq(a.operatorId, op.id)), + }); + if (!account) return; + + // Cascading FKs (reminder_runs + reminder_targets + reminder_messages) clean up. + // pg-boss job for this reminder will fire and find the row gone (soft cancel). + await db.delete(reminders).where(eq(reminders.id, reminderId)); + + revalidatePath("/reminders" as any); + redirect("/reminders" as any); +} diff --git a/apps/web/src/app/reminders/[id]/delete-dialog.tsx b/apps/web/src/app/reminders/[id]/delete-dialog.tsx index 9c3d8c1..8f6c0b0 100644 --- a/apps/web/src/app/reminders/[id]/delete-dialog.tsx +++ b/apps/web/src/app/reminders/[id]/delete-dialog.tsx @@ -12,24 +12,14 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { deleteReminderAction } from "@/actions/reminders"; interface DeleteDialogProps { - deleteAction: () => Promise; + reminderId: string; } -export function DeleteDialog({ deleteAction }: DeleteDialogProps) { +export function DeleteDialog({ reminderId }: DeleteDialogProps) { const [open, setOpen] = useState(false); - const [pending, setPending] = useState(false); - - async function handleConfirm() { - setPending(true); - try { - await deleteAction(); - } finally { - setPending(false); - setOpen(false); - } - } return ( @@ -48,14 +38,16 @@ export function DeleteDialog({ deleteAction }: DeleteDialogProps) { - +
+ + +
diff --git a/apps/web/src/app/reminders/[id]/page.tsx b/apps/web/src/app/reminders/[id]/page.tsx index e27cb49..8500e9e 100644 --- a/apps/web/src/app/reminders/[id]/page.tsx +++ b/apps/web/src/app/reminders/[id]/page.tsx @@ -73,13 +73,6 @@ function StatusPill({ status }: { status: string }) { ); } -// --------------------------------------------------------------------------- -// Server action (no-op placeholder — real delete wired in Task 19) -// --------------------------------------------------------------------------- -async function _deleteReminderStub() { - "use server"; - // wired in Task 19 -} // --------------------------------------------------------------------------- // Page @@ -242,7 +235,7 @@ export default async function ReminderDetailPage({ params }: Props) { {/* Action footer */}
- +
);