Consolidate audit-fixes branch: webhooks, integrations, and deploy hardening
Batch commit of the pending working tree on security/audit-fixes-2026-07. Major areas: - Outbound webhooks / Zapier: schema + signed delivery with retries, public v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain. - Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when Spaces is unconfigured instead of silently using ephemeral disk. - Integrations & features (concurrent work): accounting (QuickBooks/Xero), e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding, expanded legal pages. - DB migrations 0006–0009. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
969d5d4c8a
commit
c9968531e4
@@ -1,52 +1,125 @@
|
||||
import { redirect } from "next/navigation"
|
||||
import { and, eq, gte, lte } from "drizzle-orm"
|
||||
import { db } from "@/lib/db"
|
||||
import { rent_payments, leases } from "@/lib/db/schema"
|
||||
import { profiles, rent_payments, leases, inspections } from "@/lib/db/schema"
|
||||
import { getSessionUser } from "@/lib/session"
|
||||
import { CalendarClient } from "./calendar-client"
|
||||
import { getAccountContext } from "@/lib/account"
|
||||
import { CalendarClient, type CalEvent } from "./calendar-client"
|
||||
|
||||
export const metadata = { title: "Calendar" }
|
||||
|
||||
const RENT_COLOR = (s: string) =>
|
||||
s === "paid" ? "#10b981" : s === "overdue" ? "#ef4444" : "#6366f1"
|
||||
const LEASE_COLOR = "#f59e0b"
|
||||
const INSPECTION_COLOR = "#14b8a6"
|
||||
|
||||
export default async function CalendarPage() {
|
||||
const user = await getSessionUser()
|
||||
if (!user) redirect("/login")
|
||||
|
||||
const now = new Date()
|
||||
const rangeStart = new Date(now.getFullYear(), now.getMonth() - 1, 1)
|
||||
const rangeEnd = new Date(now.getFullYear(), now.getMonth() + 3, 0)
|
||||
const ctx = await getAccountContext(user.id)
|
||||
const ownerId = ctx.ownerId
|
||||
|
||||
const [payments, leaseList] = await Promise.all([
|
||||
const now = new Date()
|
||||
const rangeStart = new Date(now.getFullYear(), now.getMonth() - 3, 1)
|
||||
const rangeEnd = new Date(now.getFullYear(), now.getMonth() + 12, 0)
|
||||
const iso = (d: Date) => d.toISOString().slice(0, 10)
|
||||
|
||||
const [payments, leaseList, inspectionList, profile] = await Promise.all([
|
||||
db.query.rent_payments.findMany({
|
||||
where: and(
|
||||
eq(rent_payments.user_id, user.id),
|
||||
gte(rent_payments.due_date, rangeStart.toISOString().slice(0, 10)),
|
||||
lte(rent_payments.due_date, rangeEnd.toISOString().slice(0, 10))
|
||||
eq(rent_payments.user_id, ownerId),
|
||||
gte(rent_payments.due_date, iso(rangeStart)),
|
||||
lte(rent_payments.due_date, iso(rangeEnd))
|
||||
),
|
||||
columns: { id: true, due_date: true, amount: true, status: true },
|
||||
with: {
|
||||
tenant: { columns: { first_name: true, last_name: true } },
|
||||
},
|
||||
with: { tenant: { columns: { first_name: true, last_name: true } }, property: { columns: { name: true } }, unit: { columns: { unit_number: true } } },
|
||||
}),
|
||||
db.query.leases.findMany({
|
||||
where: and(
|
||||
eq(leases.user_id, user.id),
|
||||
gte(leases.lease_end, new Date().toISOString().slice(0, 10))
|
||||
),
|
||||
where: and(eq(leases.user_id, ownerId), eq(leases.status, "active")),
|
||||
columns: { id: true, lease_end: true },
|
||||
with: {
|
||||
tenant: { columns: { first_name: true, last_name: true } },
|
||||
property: { columns: { name: true } },
|
||||
},
|
||||
with: { tenant: { columns: { first_name: true, last_name: true } }, property: { columns: { name: true } } },
|
||||
}),
|
||||
db.query.inspections.findMany({
|
||||
where: and(eq(inspections.user_id, ownerId), gte(inspections.date, iso(rangeStart)), lte(inspections.date, iso(rangeEnd))),
|
||||
columns: { id: true, date: true, type: true, status: true },
|
||||
with: { property: { columns: { name: true } }, unit: { columns: { unit_number: true } } },
|
||||
}),
|
||||
db.query.profiles.findFirst({ where: eq(profiles.id, ownerId), columns: { calendar_token: true } }),
|
||||
])
|
||||
|
||||
const events: CalEvent[] = [
|
||||
...payments.map((p) => {
|
||||
const who = `${p.tenant?.first_name ?? ""} ${p.tenant?.last_name ?? ""}`.trim() || "Tenant"
|
||||
const color = RENT_COLOR(p.status)
|
||||
return {
|
||||
id: `rent-${p.id}`,
|
||||
title: `${who} · $${Number(p.amount).toLocaleString("en-US")}`,
|
||||
start: p.due_date!,
|
||||
allDay: true,
|
||||
backgroundColor: color,
|
||||
borderColor: color,
|
||||
editable: ctx.canWrite,
|
||||
extendedProps: {
|
||||
type: "rent" as const,
|
||||
entityId: p.id,
|
||||
status: p.status,
|
||||
subtitle: `${p.property?.name ?? ""}${p.unit ? ` · Unit ${p.unit.unit_number}` : ""}`,
|
||||
href: "/rent",
|
||||
},
|
||||
}
|
||||
}),
|
||||
...leaseList.map((l) => {
|
||||
const who = `${l.tenant?.first_name ?? ""} ${l.tenant?.last_name ?? ""}`.trim() || "Tenant"
|
||||
return {
|
||||
id: `lease-${l.id}`,
|
||||
title: `Lease ends: ${who}`,
|
||||
start: l.lease_end!,
|
||||
allDay: true,
|
||||
backgroundColor: LEASE_COLOR,
|
||||
borderColor: LEASE_COLOR,
|
||||
editable: false,
|
||||
extendedProps: {
|
||||
type: "lease" as const,
|
||||
entityId: l.id,
|
||||
subtitle: l.property?.name ?? "",
|
||||
href: `/leases/${l.id}`,
|
||||
},
|
||||
}
|
||||
}),
|
||||
...inspectionList.map((ins) => {
|
||||
const label = ins.type.replace("_", "-")
|
||||
return {
|
||||
id: `insp-${ins.id}`,
|
||||
title: `${label} inspection`,
|
||||
start: ins.date!,
|
||||
allDay: true,
|
||||
backgroundColor: INSPECTION_COLOR,
|
||||
borderColor: INSPECTION_COLOR,
|
||||
editable: ctx.canWrite,
|
||||
extendedProps: {
|
||||
type: "inspection" as const,
|
||||
entityId: ins.id,
|
||||
status: ins.status,
|
||||
subtitle: `${ins.property?.name ?? ""}${ins.unit ? ` · Unit ${ins.unit.unit_number}` : ""}`,
|
||||
href: "/inspections",
|
||||
},
|
||||
}
|
||||
}),
|
||||
]
|
||||
|
||||
const base = process.env.NEXT_PUBLIC_APP_URL ?? ""
|
||||
const subscribeUrl = profile?.calendar_token ? `${base}/api/calendar/${profile.calendar_token}.ics` : ""
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-bold text-white">Calendar</h2>
|
||||
<p className="text-sm text-white/40 mt-0.5">Rent due dates and lease expirations at a glance</p>
|
||||
<p className="text-sm text-white/40 mt-0.5">
|
||||
Rent due dates, lease expirations, and inspections — subscribe to sync with Google, Apple, or Outlook.
|
||||
</p>
|
||||
</div>
|
||||
<CalendarClient payments={payments ?? []} leases={leaseList ?? []} />
|
||||
<CalendarClient events={events} canWrite={ctx.canWrite} subscribeUrl={subscribeUrl} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user